Skip to content
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

How do you count based on user profile which is stored in a different collection? #71

Closed
Ajaxsoap opened this issue Oct 12, 2015 · 17 comments

Comments

@Ajaxsoap
Copy link

Hi,

I have a collection which has a user profile field, i grab the user profile using autovalue on collection2.

How do I count the published document of a user based on his profile?

users profile has

company and branch

Expected result is different per user.

Appreciate any help.

@boxofrox
Copy link
Contributor

I believe this is answered here. Let me know if it doesn't.

@Ajaxsoap
Copy link
Author

Thanks for the response @boxofrox !

it didn't work :(

I have an idea on how to solve this problem, which I filed an issue here on autoform and on Meteor forums here.

Basically, I will add hooks after doc insert then insert an autoincrement count field on users profile. This way, i can use your countFromField method.

@boxofrox
Copy link
Contributor

The OP is a bit vague on details, so I may not understand what you're trying to achieve.

Based on your forum question, I don't think it's necessary to add a count field to your documents.

Provide an example of your Meteor.publish routine used to grab the user profile and publish the count, then I can give specific corrections.

@Ajaxsoap
Copy link
Author

Thanks @boxofrox for looking into my problem.

What I am trying to achieve is to count the documents of a user inserted into the collection.

But the problem is, I have 3 different roles, admin, HQ and Branch.

_Admin_ i can easily count the all the documents which is the expected behaviour mycollection.find().count().

_HQ_ (Company Headquarters) - count only the documents inserted by HQ and it's branches.

_Branch_ (Company branch) - count only the documents inserted by branch.

I already grab the users profile (company and branch) like this:

company: {
      type: String,
      optional: true,
      autoform: {
        class: 'hidden'
      },
      autoValue: function() {
      if (this.isInsert) {
        var user = Meteor.users.findOne({ "_id": this.userId }, { fields: { "profile": 1 } });
        if (user) {
          return user && user.profile.company;
        }
      } else {
        this.unset();  // Prevent user from supplying their own value
      }
    }
    },
    branch: {
      type: String,
      optional: true,
      autoform: {
        class: 'hidden'
      },
      autoValue: function() {
      if (this.isInsert) {
        var user = Meteor.users.findOne({ "_id": this.userId }, { fields: { "profile": 1 } });
        if (user) {
          return user && user.profile.branch;
        }
      } else {
        this.unset();  // Prevent user from supplying their own value
      }
    }
    },

Here's publish function (for HQ):

Meteor.publish("enrollmentsByCompany", function(company, limit) {
    Counts.publish(this, 'companyCount-' + company, Enrollments.find({company: company}), {noReady: true});
    if (limit) {
        return Enrollments.find({company: company});
    } else {
        return Enrollments.find({company: company}, {limit: limit}) ;
    }
});

My subscription:

Tracker.autorun(function() {
   Meteor.subscribe("enrollmentsByCompany");
});

Helpers

"enrollmentsComp anyCount": function(){
     return Counts.get(companyCount);
}

I don't know how do I inject the roles to limit the count based on user profile. Here's how I check for a user roles:

var user = Meteor.users.findOne({"_id": this.userId},{fields:{profile: 1}});
var roles = Roles.userHasRole( this.userId, "HQ" );

@Ajaxsoap
Copy link
Author

Here's the user profile looks like:

"profile": {
  "name" : "User Name",
  "company" : "KRkwBeicgL5ayedGa",
  "branch" : "66DP8rYtrSGWtDFMK",
 "picture" : {
   "fileId" : "rQQHP4YEuM2KQvTDd",
   "url" : "http://localhost:3000/gridfs/data/id/8252332c839100f950b97794"
  }
}

@boxofrox
Copy link
Contributor

Thanks for the details. Looks like you have few collections (Meteor.users, Roles, and Enrollments).

If you require normalized data among your collection and rely on a join between Roles and Enrollments, then I'm not sure I have an answer. @tmeasday mentioned in another issue that coding server-side joins is best to avoid. If your joins and subsequent counts are client-side, then publish-counts isn't of much use.

If you have no normalization policy--and it's my understanding that with MongoDB you should not--then we might consider including company and branch in the Enrollments collection.

So your Enrollments document would look like:

{
  "company": "KRkwBeicgL5ayedGa",
  "branch": "66DP8rYtrSGWtDFMK",
  ... other Enrollment fields
}

Then your publish function might be:

Meteor.publish("enrollmentsByAdmin", function (limit) {
    Counts.publish(this, 'enrollments', Enrollments.find({}), {noReady: true});
    if (limit) {
        return Enrollments.find({});
    } else {
        return Enrollments.find({}, {limit: limit}) ;
    }
});

Meteor.publish("enrollmentsByCompany", function(company, limit) {
    Counts.publish(this, 'enrollments', Enrollments.find({company: company}), {noReady: true});
    if (limit) {
        return Enrollments.find({company: company});
    } else {
        return Enrollments.find({company: company}, {limit: limit}) ;
    }
});

Meteor.publish("enrollmentsByBranch", function(branch, limit) {
    Counts.publish(this, 'enrollment', Enrollments.find({branch: branch}), {noReady: true});
    if (limit) {
        return Enrollments.find({branch: branch});
    } else {
        return Enrollments.find({branch: branch}, {limit: limit}) ;
    }
});

And your subscription logic will be:

var user = Meteor.users.findOne({"_id": this.userId},{fields:{profile: 1}});

if (Roles.userHasRole(this.userId, "Admin"))
    Meteor.subscribe("enrollmentsByAdmin", 5);
else if (Roles.userHasRole(this.userId, "HQ"))
    Meteor.subscribe("enrollmentsByCompany", user.profile.company, 5);
else if (Roles.userHasRole(this.userId, "Branch"))
    Meteor.subscribe("enrollmentsByBranch", user.profile.branch, 5);

console.log("enrollment count: ", Counts.get("enrollments"));

Note: this code does not react to changes in a user's role. If a user is assigned Branch and later HQ, their counts will continue to use Branch until the user reloads the page.

Note: also, I assume a user only subscribes to one enrollment publishing, so all three counts use the 'enrollments' name.

Sorry the response took so long. I had to rewrite this post a few times as I grokked your code examples.

@Ajaxsoap
Copy link
Author

Wow! Thanks @boxofrox for this!

I don't have any normalization policy as i don't know how to normalized a collection.

Yeah, on Enrollments i have this:

{
  "company": "KRkwBeicgL5ayedGa",
  "branch": "66DP8rYtrSGWtDFMK",
  ... other Enrollment fields
}

BTW, I am using orionjs as my admin.

I will try this later at home as this is not my main job.

I will let you know and hopefully close this issue 👍

@Ajaxsoap
Copy link
Author

The console log output is all 0 even on admin, so I've modified the admin publish to:

Meteor.publish("enrollments", function(){
    Counts.publish(this, 'enrollmentsCount', Enrollments.find({}), {noReady: true});
    return Enrollments.find();
});

I've post an issue on nicolas lopez roles repo, he is the author of the package as well as orionjs here.

I will wait for his comment if has something to do with his package.

Thanks a lot again @boxofrox for helping me!

@boxofrox
Copy link
Contributor

FYI. You can move most of the processing to the server.

Update your server-side publish routine to:

function getEnrollmentQuery (user) {
    if (Roles.userHasRole(user._id, "Admin"))
        return {};
    else if (Roles.userHasRole(user._id, "HQ"))
        return {company: user.profile.company};
    else if (Roles.userHasRole(user._id, "Branch"))
        return {branch: user.profile.branch};
    else
        throw new Error('getEnrollmentQuery:  unknown role for user (' + user._id + ')');
}

Meteor.publish("enrollments", function (limit) {
    var user  = Meteor.users.findOne({"_id": this.userId}, {fields:{profile: 1}});
    var query = getEnrollmentQuery(user);

    Counts.publish(this, 'enrollmentsCount', Enrollments.find(query), {noReady: true});
    if (limit) {
        return Enrollments.find(query);
    } else {
        return Enrollments.find(query, {limit: limit}) ;
    }
});

And your subscription routine becomes:

Meteor.subscribe("enrollments", 5);
console.log("enrollment count: ", Counts.get("enrollments"));

This version is more secure since your users can't manually invoke the Meteor.subscribe("enrollmentsByAdmin"); via the browser console.

@Ajaxsoap
Copy link
Author

Hi @boxofrox ,

It worked! :)

However, it seems that it didn't count during first load, i have to reload the page to see the actual count.

BTW, I put the subscription on the router.

what is the significance of 5 inside subscription? ("enrollments", 5)

Thanks!

@boxofrox
Copy link
Contributor

...it didn't count during first load, i have to reload the page to see the actual count.

I don't know what you mean by first load. It sounds like there is some task performed by the browser/server before the user can visit the page in question. Once there a reload fixes the count.

If you can reproduce that behavior, what happens when you visit the page (first load), open the dev-tools console, and run Counts.get("enrollmentsCount");? Do you get the wrong number, or the correct number?

If the latter, then your template may not be reactively updating when the initial count is generated after the page loads.

what is the significance of 5 inside subscription?

5 is an arbitrary number I used for the limit parameter of the publish function:

Meteor.publish('enrollments', function (limit) {});

You're welcome to leave it out.

@Ajaxsoap
Copy link
Author

I mean by first load is after logging in the application, after the user logged in, console log 0 count, then reload/refresh browser, it will display the actual document count.

The subscription is in the router, It didn't work, then I moved it in the main template logic (dashboard.js) and wrapped in Tracker.autorun function, still, same effect, I have to reload the page to see the actual document count.

What is the effect if I change the limit 5 to other number or remove it? Sorry for the newbie question. I'm trying to understand the concept.

Thanks @boxofrox.

@boxofrox
Copy link
Contributor

I mean by first load is after logging in the application

Ah, that makes sense. I've not done user logins or used routes, so I'm not sure where the problem would be.

I think we've sorted out the problem for this issue and found a new issue with setting up subscriptions with the router. If you're working with your meteor-insurance repository, push the changes you have so far and I can try reproducing the issue on my end. If you'd rather keep your commits private, that's fine, too.

What is the effect if I change the limit 5 to other number or remove it?

limit is fed into the Collection.find, so in my example I limited my application to returning 5 documents from the Enrollments collection. If you bump it to 10, then you'll get 10 documents on the client once the subscription is ready. It's a way to avoid dumping your entire collection on every client. (see Collection.find)

Typically our datasets are unbounded, and we don't want every client downloading 100,000+ documents, so we limit them to a smaller subset. If you wanted to add pagination so the user could view 25 records or so per page, you could add a skip (see Collection.find) parameter to specify which group of 25 from your 100,000+ records to show. This does get a bit complicated because you'll likely want to stop the subscription for the previous page when you subscribe to view the next page, so you'll need to store subscription handles somewhere.

If you never use the limit parameter, then you don't need publish-counts. You can always use Enrollments.find({}).count() on the client (whose record set was limited by the query in publish according to their role). The purpose of publish-counts is to count records on the server that won't be sent to the client, then send only the count to the client.

Sorry for the newbie question. I'm trying to understand the concept.

No worries

@Ajaxsoap
Copy link
Author

Hi @boxofrox ,

Here's the repo for repro here.

Btw, i have other collection that require a document count also but you can ignore that, as soon we solve the first issue, I will try to work that.

I can now understand the limit.

You can uncomment the code on server > fixture.js for the admin credential.

Thanks Again!

@boxofrox
Copy link
Contributor

Let's move this conversation into gitter for a bit. i think you'll have to set up the room, first.

@Ajaxsoap
Copy link
Author

Hi @boxofrox ,

I've managed to achieve what I want. I've put my subs on my router and it worked.

However, i have this one last requirement which I will file later.

Thanks for your help!

@boxofrox
Copy link
Contributor

My pleasure. Cheers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants