To throw some context around our models: an organization has users (with different roles), and users have blog posts.
I have a use case where I need to populate two tables in our UI with post usage data by users in an organization (i.e. posts as in blog posts). One table shows a summary of the total post count for the entire organization grouped by the role of the user (e.g. admins, editors, and writers). The other table, though, would show all users and the post count for each one of them, regardless of their role.
However, this API endpoint/s will also be public and used by organization admins to determine post usage, so we want it to be as intuitive as possible.
We already have an endpoint that returns data about users (email, name, etc.)
The format is akin to:
{
current_page: 1,
per_page: 50,
next_page: 2,
previous_page: null,
total_pages: 3,
total_count: 150,
users: [
{
id: 1,
name: "John Doe",
email: "email#example.com",
...
},
...
]
}
The aggregated data we need is just the post count for an organization grouped by user roles, as well as the post count per user (for all users). That is, the number of posts that a user has published over a time period (which would be provided in the URL params).
We've bounced this idea for a second, and have come up with some alternatives, the one making more sense so far being serving data through one endpoint that allows for aggregating data based on defined aggregation patterns.
For example, we could do the following to get the information for the first table, which needs aggregated data for an entire organization:
# /usage/posts?from=&to=&aggregation=user_role
{
usage: {
admin: {
post_count: admin_posts.count,
published_post_count: admin_posts.published.count
},
editor: {
post_count: editor_posts.count,
published_post_count: editor_posts.published.count,
},
writer: {
...
}
}
}
Then for the second table, which needs to include all users in an organization and their post counts, we can do something along the lines of:
# /usage/posts?from=&to=&aggregation=user(default)
{
current_page: 1,
per_page: 50,
next_page: 2,
previous_page: null,
total_pages: 3,
total_count: 150,
usage: [
{
id: user.id,
post_count: user.posts.count,
email: user.email,
name: user.name,
role: user.role,
...
},
{
id: user.id,
post_count: user.posts.count,
email: user.email,
name: user.name,
role: user.role,
...
},
...
]
}
Does this payload structure seem intuitive? Not just for our own usage and to be able to populate our UI, but also for our users to fetch data from that they can use however they see fit.
I'd appreciate any feedback and thoughts!
Related
I am creating a Rails api for teachers to rank students based on certain criteria. I have four models: classroom, student, criterion and rank.
Students/Criteria are many to many through Rank
Students/Classroom are many to many
Rank is a join table between Student/Criteria with the additional field of rank, which is an integer between 1-4.
I am able to return the list of Students belonging to a Classroom in a response (1 relation deep) by allowing Classroom.students through in my classroom serializer. How can I return each student's ranks nested within students in my Classroom response (2 relations deep) from my API? Ideal response as below:
Classroom_A:
{
id: "123",
name: "classroom A",
students: [
{ id: "456"
name: Juanita,
gender: female,
ranks: [
{ id: "789",
student_id: "456",
name: "willingness to help others",
rank: "4"
},
{ id: "101",
student_id: "456",
name: "Leadership",
rank: "3"
} ...
]
},
{ id: "232"
name: Billy,
gender: male,
ranks: [
{ id: "789",
student_id: "232",
name: "willingness to help others",
rank: "3"
},
{ id: "101",
student_id: "232",
name: "Leadership",
rank: "3"
} ...
]
}
]
}
Thanks in advance.
A similar question was posted at Rails: Serializing deeply nested associations with active_model_serializers (thanks for the link #lam Phan)
However, the most upvoted answer for that post was not super clear and there was no code example. Super dissapointing. I also looked into the JsonApi adapter and was not able to get the solution I was looking for. In the end I ended up taking the same route as the OP for the linked question: in the serializer I wrote my own function and manually sideloaded the other data that I wanted. I was hoping to do this the "Rails way" but in the end I chose to just get it done.
class ClassroomSerializer < ApplicationSerializer
attributes :id, :name, :school_name, :students
def students
customized_students = []
object.students.each do |student|
custom_student = student.attributes
custom_student['ranks'] = student.student_ranks
customized_students.push(custom_student)
end
customized_students
end
end
I have the following n to n (done with mongoid gem) with two collections books and publishers:
{
name: "O'Reilly Media",
founded: 1980,
location: "CA",
books: [123456789, 234567890, ...]
}
{
_id: 123456789,
title: "MongoDB: The Definitive Guide",
author: [ "Kristina Chodorow", "Mike Dirolf" ],
published_date: ISODate("2010-09-24"),
pages: 216,
language: "English"
}
{
_id: 234567890,
title: "50 Tips and Tricks for MongoDB Developer",
author: "Kristina Chodorow",
published_date: ISODate("2011-05-06"),
pages: 68,
language: "English"
}
I need to return in one query the publishers, but separated in documents, like this:
{
name: "O'Reilly Media",
founded: 1980,
location: "CA",
book: 123456789 # or books:[123456789]
}
{
name: "O'Reilly Media",
founded: 1980,
location: "CA",
book: 234567890 # or books:[123456789
}
I want to this inside mongo in a query, actually I do it in the rabl file modifying the collection, but this is not good por gaination and using in other representations, So I want to do this transformation in Mongo, not in ruby, Or maybe I should change the query calling for books instead on publishers.
This is the code in ruby:
#publishers is a mongoid::Criteria
#publishers = #publishers.collect do |s|
s.books.count > 1 ? s.publisher_separate_by_books : s
end.flatten
class Publisher
has_and_belongs_to_many :books, inverse_of: :books, dependent: :nullify
def publisher_separate_by_books
books.map {|i| Publisher.new({name: name, founded: founded, location: location, books: [i]})}
end
end
How can achieve this in mongo query
There is no advantage to expanding the query result like that in the database server (any database server). If you wanted to perform additional operations per book in the query (which in case of MongoDB would involve the aggregation pipeline, and for relational databases JOIN operations) then it would make sense. But simply expanding a field like that in the database is wasteful.
MongoDB does support this operation via unwind (https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind), but then you lose the ability to query the models using DSL provided by Mongoid and have to construct the aggregation pipelines instead.
I noticed that when using will_paginate 20 results a page, joining multiple tables group_by "nickname", the output is paginated but 3 "nicknames" only showing (this makes sense since pagination counted the output before group by) but how can I solve that? also, I want to display the output like that and limit the number of items per page based on "nickname" column: please note that the "data" table has many "preferences". (data.id = preferences.data_id)
{
"totalCount": 123,
"pageInfo": {
"currentPage": 1,
"nextPage": 2,
"lastPage": 8
},
"results": [
{
"data": {
"id": 1,
"nickname": "foo"
},
"preferences": [
{
"id": 4479,
"created_at": "2019-05-21T00:39:45.772Z",
"updated_at": "2019-05-21T00:39:45.772Z",
"title": "Check Database",
...
},
...
]
},
...
]
}
data_res = Data.paginate(page: params[:page], per_page:
20).joins("INNER JOIN preferences ON data.id =
preferences.data_id").select("data.*, preferences.*").order(id: :asc)
data_group_by = data_res.group_by { |r| r.nickname }
respond_to do |format|
format.any
format.json {
render :json => {
:totalCount => data_res.total_entries,
:pageInfo => {
:currentPage => data_res.current_page,
:nextPage => data_res.next_page,
:total_pages => data_res.total_pages,
:per_page => data_res.per_page,
},
:results => data_res
}
}
end
If I'm understanding your question correctly(probably not), pagination says it has 20 records, but you're only seeing 3 records being returned because they're grouped?
However, what you want is 20 records, with 20x preferences grouped?
If that's the case, I think you're probably overcomplicating your query.
I don't think you should use the select("data.*, preferences.*") because it basically just adds a new record per preference fetched, so the preferences is probably the determinant of how many records you're getting rather than data on which you're paginating on + you're dynamically adding additional methods to each of the data returned to account for the preferences
data_res.group_by { |r| r.nickname } seems unnecessary, unless you have data records that are not unique, in which case I'd question the reason for grouping them by that.
In my opinion, if nicknames are unique, i.e there can only be 1 data record with the same nickname, here's what I'd propose
class Data
has_many :preferences
end
class Preference
belongs_to :data
end
joins-ing and includes-ing here to ensure the preferences are eager loaded,
while conforming to your existing query of only fetching data with preferences
data_res = Data.joins(:preference).includes(:preference).paginate(page: params[:page], per_page: 20).order(id: :asc) # this should give you 20 records of data that has preferences
Then your serializer can do the rest of work to ensure your data is properly mapped and that your preferences are on the same level(there are several ways to achieve that with any serialization package), e.g.
class DataPreferencesSerializer < AMS
attributes :data
:preferences # or can be has_many :preferences serializer
def data
{id: object.id, nickname: object.nickname }
end
def preferences
object.preferences
end
end
results= ArraySerializer.new(data_res, each_serializer: DataPreferencesSerializer, root: false)
Like I said, there are several ways of achieving the serialization, so the implementation above is just an idea of the approach you might take and not a specific implementation.
PS: Your INNER JOIN ensures that all the returned data records have an associated preferences, so any data that doesn't have at least one preference that is probably excluded from the records you get back.
How in rails, do you call update_attributes() on a active record object, and have it create appropriate rows in one-to-many associated table based on an array param.
Is this possible? Or do I need to manually loop through that param's array and insert the many rows manually via create_record() etc?
To be clear, I might have users table with 1-to-many addresses. I want to call users.update_attributes() passing in the user details to be updated, but also provide an array of addresses mapping to the addresses table.
In your User model:
model User
has_many :addresses
accepts_nested_attributes_for :addresses
end
now user can be created with this set of params
params = { user: {
name: 'Dimitri', address_attributes: [
{ country: 'Georgia', city: 'Abasha', line: '35 Kacharava Str.' },
{ country: 'USA', city: 'Los Angeles', line: '10 Infinite Loop' },
{ country: '', _destroy: '1' } # this will be ignored
]
}}
User.create(params[:user])
more details can be found from here.
I have a User, and that User has a bunch of contacts (some contacts have "name" and all have "email"). Contacts have a field for an image_url, and I want to show a picture for that contact. I want to pull those pictures from Facebook.
Suppose User.first has email "joe#test.com", which is registered with Facebook.
User has_many contacts && Contacts belongs_to User.
User.first.contacts
#<Contact id: 1, ctct_name: "Stacy Blah", ctct_email: "stacy.blah#gmail.com", user_id: 1, img_url: nil>,
#<Contact id: 2, ctct_name: nil, ctct_email: "dick.tracy#gmail.com", user_id: 1, img_url: nil>,
#<Contact id: 3, ctct_name: "John Doe", ctct_email: "john_doe#gmail.com", user_id: 1, img_url: nil>
I have fb_graph working on my site (using Devise :omniauthable).
When User.first connects to Facebook, I have access to their fb token. So I can do this:
facebook_user = FbGraph::User.new('me', :access_token => access_token).fetch
user_friends = facebook_user.friends
Now what is the best way to find or match User.first.contacts to user_friends? Ideally, I would simply search Facebook connect for emails, like this:
email = Contact.first.ctct_email
search = FbGraph::User.search(email, :access_token => access_token = user['credentials']['token'])
Contact.first.img_url = search[0].picture # or something like this
BUT Facebook API doesn't expose user_friends emails! So, what can I do?
I've been trying to match based on names, but I run into two problems: (1) contacts.name may be nil, and (2) user_friends.select {|f| f.name.include?("John Doe")} is too restrictive where the names don't match exactly (e.g., contact.name = "John Doe" but Facebook has this person as "John X. Doe").
I have also tried searching FBGraph::User.search with name parameter = User.first.contacts[2].name.
search = FbGraph::User.search("John Doe", :access_token => access_token = user['credentials']['token'])
But then I have a severe disambiguation problem -- searching "John Doe" brings up thousands of Facebook users, and they aren't limited to the user_friends!
Surely this is possible. I've seen services that can pull an image from Facebook for a given contact, with as little information as an email.
Thank you for your help!!
You can search for the Facebook user by their email address:
https://graph.facebook.com/search?q=zuck#fb.com&type=user&access_token=...
Note that as of 12/21/11 there is an accepted bug that this feature currently isn't working.