I am using meteor and this is my schema, each is a separate collection:
Courses has many lectures
Lectures have many questions
Questions have many answers
I want 1 page where I can display a given course's lectures, questions, and answers. I can display a course's lectures no problem but I have issues with displaying further nested items. I'd ideally like to have:
Lecture has courseId
Answer has lectureId (but not courseId)
Question has answerId (but not lectureId or courseId)
Is that wise or should I embed courseIds and lectureIds in all child components? This is my iron router, I tried to extend the same idea that worked with nesting lectures with questions but I hit a stumbling block with how to feed the subscriptions the lecturesId:
Router.route('/courses/:_id', {
name: 'CoursePage',
waitOn: function(){
return [
Meteor.subscribe('singleCourse', this.params._id),
Meteor.subscribe('lectures', this.params._id),
Meteor.subscribe('questions', this.params._id)
];
},
data: function() {
return Courses.findOne(this.params._id);
}
});
This is the subscriptions for the course page, again with my stumbling block of not really knowing how to feed in a lectureId:
Template.CoursePage.helpers({
Lectures: function() {
return Lectures.find({courseId: this._id});
},
Questions: function(lectureId) {
return Questions.find({courseId: this._id, lectureId: lectureId});
}
});
Can anyone recommend a good way to do this 4 level nesting for a single page? I think that I am missing something obvious but I can't quite find a good example with google searching.
Thanks!
You can Publish Composite package for this. See the following sample code and edit as per your collection schemas,
Meteor.publishComposite('singleCourse', function (courseId) {
return [{
find: function() {
return Courses.find({ id: courseId});
}
}, {
find: function() {
return Lectures.find({ courseId: courseId});
},
children: [{
find: function(lecture) {
return Questions.find({ lectureId: lecture.id });
},
children: [{
find: function(question) {
return Answers.find({ questionId: question.id });
}
}]
}}
}]
});
Then in your router, you can simply make one subscription call,
Router.route('/courses/:_id', {
name: 'CoursePage',
waitOn: function(){
return [
Meteor.subscribe('singleCourse', this.params._id)
];
},
data: function() {
return Courses.findOne(this.params._id);
}
});
This is one of the best packages (if not the best) as of now to reactively publish set of related documents from different collections.
There are some known issues while doing these kind of reactive joins but for smaller datasets, this works without any problem.
Hope it helps.
Mongo can support using aggregation. $lookup will let you connect and gather data between your collections like an SQL join.
Using this in meteor requires using an external mongo ($lookup is new as of Mongo 3.2, meteor's Mongo is still 2.6.7) and a package such as the meteorhacks:aggregate package. There are other packages that address this, as mentioned in the comments, aggregate is just what I've used; with it you call Courses.aggregate(...) per the mongo aggregation documentation to produce the data that you require.
In my use, I had a Meteor method defined that took filter parameters as arguments
'aggregateReport':function(filterPersonnel, filterCourse, filterQuarter){
return Personnel.aggregate([{$match: filterPersonnel}, {$unwind: "$courses"},
{$lookup: {from: "courses", localField: "courses", foreignField: "_id",
as: "course_docs"}}, {$unwind: "$course_docs"}, {$match: filterCourse},
{$match: filterQuarter}]);
The Personnel have: country, course date, lastname, fullname, ..., course #, course. (The ellipses covers non-relevant to the query). The above queries Personnel per the filter, spools it out to one record per course (this is a transcript type of view for many people in a program), then adds the information from Courses as course_docs to the returned Personnel, and then filters by course parameters and date parameters. code and dependencies were meteor 1.2; Feb 2016
Related
I am currently using ReactiveAggregate to find a subset of Product data, like this:
ReactiveAggregate(this, Products, [
{ $match: {}},
{ $project: {
title: true,
image: true,
variants: {
$filter: {
input: "$variants",
as: "variant",
cond: {
$setIsSubset: [['$$variant.id'], user.variantFollowing]
}
}
}
}}
], { clientCollection: 'aggregateVariants' }
As you can see, a variant is returned if user.variantFollowing matches. When a user 'follows' a product, the ID is added to their object. However, if I understand correctly, this is not triggering ReactiveAggregate to get the new subset when this happens. Only on a full page refresh do I get the correct (latest) data.
Is this the correct way to approach this?
I could store the user's ID as part of the Product object, but the way this would be stored would be nested two places, and I think I would need the Mongo 3.5 updates to then be able to accurately update this. So i'm looking for how to do this in Meteor 1.5+ / Mongo 3.2.12
So, I've been able to get there by adding autorun to the subscription of the aggregate collection, like this:
Template.followedProducts.onCreated(function() {
Meteor.subscribe('products');
this.autorun(() => {
Meteor.subscribe('productsFollowed');
});
... rest of function
For context, productsFollowed is the subscription to retrieve aggregateVariants from the original question.
Thanks to robfallows in this post: https://forums.meteor.com/t/when-and-how-to-use-this-autorun/26075/6
My site allows users to apply by connecting their google account, as soon as an account is created they're given a "pending" role (using alanning:roles package). I would like to have a table for admins to see when new applicants have applied, from there the admin can properly manage the users application (change role to accepted, declined, etc.). So, I have created my table, but it's showing all users, I'm wondering if someone knows a way to make it so only users with the "pending" role are shown in my table?
Here is what I have so far:
TabularTables.ManageApplications = new Tabular.Table({
name: 'ManageApplications',
collection: Meteor.users,
allow: function (userId) {
return Roles.userIsInRole(userId, 'admin');
},
autoWidth: false,
oLanguage: {
"sSearch": "Search: "
},
columns: [
{ data: //column details here },
{ data: //column details here },
{ data: //column details here },
{ data: //column details here },
{ data: //column details here },
],
});
This works, but it shows every user (not just users with the "pending" role).
I then created this to try and publish only data for pending users:
Meteor.publish("pendingUsers", function() {
var isAdmin = Roles.userIsInRole(this.userId, 'admin');
if (isAdmin) {
return Roles.getUsersInRole('pending').fetch();
} else {
return null;
}
});
and subscribed by adding pub: "pendingUsers", to my table. This somewhat works, it makes it so it only shows data in the columns for "pending" role users, but, it still lists every user and just has blank spaces where the data would be.
If anyone knows how I can achieve this it would be greatly appreciated if you could give some insight as I've been stuck on this for quite a while... I believe it may have to do with "Displaying Only Part of a Collection's Data Set" in the Tabular readme, but I'm very unsure of how to set this up. Any examples or help is extremely appreciated.
This has been solved by adding the following to my table:
selector: function(userId) {
return {
_id: {$in: Roles.getUsersInRole('pending')
.map(function(user){ return user._id} ) }
}
},
Given the way that publications work it's more than likely that you have another publication and subscription which is giving you the rest of the users but with a different set of keys/fields. Since multiple publications can be running on the same collection at the same time you want to perform the same .find() on the client that your publication is giving you.
Go ahead and add a selector to your table definition as follows:
selector: function( userId ) {
return Roles.getUsersInRole('pending');
}
You don't need the .fetch() in your publication btw, Roles.getUsersInRole() already returns a cursor of Meteor.users.
Use Selector: https://github.com/aldeed/meteor-tabular#modifying-the-selector
If you want to check all the rules of such a group:
selector: function (userId) {
return 'roles.myrole': {$exists: true}};
},
Or with just a few:
selector: function (userId) {
return 'roles.myrole': {$in: ['viewer', 'editor']}};
},
I have a game built on Meteor framework. One game document is something like this:
{
...
participants : [
{
"name":"a",
"character":"fighter",
"weapon" : "sword"
},
{
"name":"b",
"character":"wizard",
"weapon" : "book"
},
...
],
...
}
I want Fighter character not to see the character of the "b" user. (and b character not to see the a's) There are about 10 fields like character and weapon and their value can change during the game so as the restrictions.
Right now I am using Session variables not to display that information. However, it is not a very safe idea. How can I subscribe/publish documents according to the values based on characters?
There are 2 possible solutions that come to mind:
1. Publishing all combinations for different field values and subscribing according to the current state of the user. However, I am using Iron Router's waitOn feature to load subscriptions before rendering the page. So I am not very confident that I can change subscriptions during the game. Also because it is a time-sensitive game, I guess changing subscriptions would take time during the game and corrupt the game pleasure.
My problem right now is the user typing
Collection.find({})
to the console and see fields of other users. If I change my collection name into something difficult to find, can somebody discover the collection name? I could not find a command to find collections on the client side.
The way this is usually solved in Meteor is by using two publications. If your game state is represented by a single document you may have problem implementing this easily, so for the sake of an example I will temporarily assume that you have a Participants collection in which you're storing the corresponding data.
So anyway, you should have one subscription with data available to all the players, e.g.
Meteor.publish('players', function (gameId) {
return Participants.find({ gameId: gameId }, { fields: {
// exclude the "character" field from the result
character: 0
}});
});
and another subscription for private player data:
Meteor.publish('myPrivateData', function (gameId) {
// NOTE: not excluding anything, because we are only
// publishing a single document here, whose owner
// is the current user ...
return Participants.find({
userId: this.userId,
gameId: gameId,
});
});
Now, on the client side, the only thing you need to do is subscribe to both datasets, so:
Meteor.subscribe('players', myGameId);
Meteor.subscribe('myPrivateData', myGameId);
Meteor will be clever enough to merge the incoming data into a single Participants collection, in which other players' documents will not contain the character field.
EDIT
If your fields visibility is going to change dynamically I suggest the following approach:
put all the restricted properties in a separated collection that tracks exactly who can view which field
on client side use observe to integrate that collection into your local player representation for easier access to the data
Data model
For example, the collection may look like this:
PlayerProperties = new Mongo.Collection('playerProperties');
/* schema:
userId : String
gameId : String
key : String
value : *
whoCanSee : [String]
*/
Publishing data
First you will need to expose own properties to each player
Meteor.publish('myProperties', function (gameId) {
return PlayerProperties.find({
userId: this.userId,
gameId: gameId
});
});
then the other players properties:
Meteor.publish('otherPlayersProperties', function (gameId) {
if (!this.userId) return [];
return PlayerProperties.find({
gameId: gameId,
whoCanSee: this.userId,
});
});
Now the only thing you need to do during the game is to make sure you add corresponding userId to the whoCanSee array as soon as the user gets ability to see that property.
Improvements
In order to keep your data in order I suggest having a client-side-only collection, e.g. IntegratedPlayerData, which you can use to arrange the player properties into some manageable structure:
var IntegratedPlayerData = new Mongo.Collection(null);
var cache = {};
PlayerProperties.find().observe({
added: function (doc) {
IntegratedPlayerData.upsert({ _id : doc.userId }, {
$set: _.object([ doc.key ], [ doc.value ])
});
},
changed: function (doc) {
IntegratedPlayerData.update({ _id : doc.userId }, {
$set: _.object([ doc.key ], [ doc.value ])
});
},
removed: function (doc) {
IntegratedPlayerData.update({ _id : doc.userId }, {
$unset: _.object([ doc.key ], [ true ])
});
}
});
This data "integration" is only a draft and can be refined in many different ways. It could potentially be done on server-side with a custom publish method.
Imagine you have a collection similar to the following...
Tests = [
{
name: 'Some Test',
questions: [
{ question: 'Answer to life, the universe, and everything?', answer: '42' },
{ question: 'What is your favorite color?', answer: 'Blue' },
{ question: 'Airspeed velocity of unladen European Swallow?', answer: '24 mph' }
]
}
];
How do you publish the entire collection except for the answer property?
I understand you can do the following to omit properties from the publish...
Meteor.publish('tests', function() {
return Tests.find({}, {fields: {name:0}});
});
But I'm not sure how to omit a property from an array property.
Thanks!
It can't be done the way you want to do it. Meteor only supports field specifiers that are 1 level deep. You can sometimes get a sub-field specifier to work, but it's not reliable.
You can put your questions into their own collection with a testId field that links them back to the test, relational style. One question per document, and then you'll be able to specify that only the question field gets published.
Meteor.publish ('questions', function(testId) {
return Questions.find({testId: testId}, {fields: {question: 1}})
});
It's not ideal, but pretty painless compared to trying to find a workaround that allows your questions to live in the test document.
There might be a way to do this manually with a more involved publish. There's a similar question here with an answer that gets into it.
I'm having trouble configuring the waitOn portion of a route where one of the subscription's parameters is determined by the value from a doc that comes from a different subscription.
The collections in play are Candidates and Interviews. An interview will have one and only one candidate. Here's some sample data:
candidate = {
_id: 1
firstName: 'Some',
lastName: 'Developer'
//other props
};
interview = {
_id: 1,
candidateId: 1
//other props
};
The route is configured as follows.
this.route('conductInterview', {
path: '/interviews/:_id/conduct', //:_id is the interviewId
waitOn: function () {
return [
Meteor.subscribe('allUsers'),
Meteor.subscribe('singleInterview', this.params._id),
// don't know the candidateId to lookup because it's stored
// in the interview doc
Meteor.subscribe('singleCandidate', ???),
Meteor.subscribe('questions'),
Meteor.subscribe('allUsers')
];
},
data: function () {
var interview = Interviews.findOne(this.params._id);
return {
interview: interview,
candidate: Candidates.findOne(interview.candidateId);
};
}
});
The problem is that I don't have a candidateId to pass to the singleCandidate subscription in the waitOn method because it's stored in the interview doc.
I've thought of two possible solutions, but I don't really like either of them. The first is to change the route to something like /interviews/:_id/:candidateId/conduct. The second is to denormalize the data and store the candidate's info in the interview doc.
Are there any other options to accomplish this besides those two?
You may get some ideas by reading this post on reactive joins. Because you need to fetch the candidate as part of the route's data, it seems like the easiest way is just to publish both the interview and the candidate at the same time:
Meteor.publish('interviewAndCandidate', function(interviewId) {
check(interviewId, String);
var interviewCursor = Interviews.find(interviewId);
var candidateId = interviewCursor.fetch()[0].candidateId;
return [interviewCursor, Candidates.find(candidateId);];
});
However, this join is not reactive. If a different candidate gets assigned to the interview, the client will not be updated. I suspect that isn't a problem in this case though.
You can change your publish function singleCandidate to take interviewId as paramater instead of candidateId and pass this.params._id
I had similar problem I managed to solve it via callback in subscribe
http://docs.meteor.com/#/basic/Meteor-subscribe
For example you have user data with city ids, and you need to get city objects
waitOn: ->
router = #
[
Meteor.subscribe("currentUserData", () ->
user = Meteor.user()
return unless user
cityIds = user.cityIds
router.wait( Meteor.subscribe("cities", cityIds)) if cityIds
)
]