With dburles:collection-helpers package you can add collection helpers on any Mongo.collection. But I can't do that on FS.Collection. I get TypeError: Object [object Object] has no method 'helpers'. Transform function doesn't work either.
var createUploader = function(fileObj, readStream, writeStream) {
fileObj.uploadedBy = Meteor.users.find({_id: fileObj.uploader});
readStream.pipe(writeStream);
};
Photos = new FS.Collection("photos", {
stores: [
new FS.Store.GridFS("photos", {transformWrite: createUploader})
],
filter: {
allow: {
contentTypes: ['image/*']
}
}
});
Can't do this? Notice when a photo is inserted from the client FS.File gets userId, hence fileObj.uploadedBy = Meteor.users.find({_id: fileObj.uploader});
Ok I know this is not the not-so-easy solution I was looking for. Since I am using publish-composite package. I can just publish users' data(only profile field) with photos. And on the client I can do template helper like this:
Template.photo.helpers({
photoUploader: function() {
var currentPhoto = Photos.findOne();
var user = Meteor.users.findOne({_id: currentPhoto.uploader});
return user.profile.name;
},
});
and
<template name="photos">
{{#each photos}}
{{> photo}}
{{/each}}
...
then
<template name="photo">
{{photoUploader}}
...
matb33-collection-helpers package works by applying a transform function to a collection. CollectionFS already has its own transform function applied, therefore you cannot overwrite that with ones from the collection helpers package.
As suggested at the issue tracker
Since CFS already applies a transform, it would not be a good idea to use collection helpers. You should be able to do pretty much the same thing by extending the FS.File prototype with your own functions, though.
You could define custom functions on the prototype. The prototype will have access to other properties of the doc through this so you would basically have the same functionality with collection helpers.
Another option would be to store the file related information on the individual file object during the insert as metadata such as:
Template.photoUploadForm.events({
'change .photoInput': function(event, template) {
FS.Utility.eachFile(event, function(file) {
var newPhoto = new FS.File(file);
newPhoto.metadata = {uploadedBy: Meteor.user().profile.name};
Photos.insert(newPhoto, function (err, fileObj) {
if (!err) console.log(fileObj._id + " inserted!")
});
});
}
});
Your code can also be rewritten to implement a beforeWrite filter instead of a transformWrite as in
Photos = new FS.Collection("photos", {
stores: [
new FS.Store.GridFS("photos", {
beforeWrite: function (fileObj) {
fileObj.metadata = {uploadedBy: Meteor.user().profile.name};
}
})
],
filter: {
allow: {
contentTypes: ['image/*']
}
}
});
Finally, you can opt to storing the ID of the user and publishing a reactive join
Photos = new FS.Collection("photos", {
stores: [
new FS.Store.GridFS("photos", {
beforeWrite: function (fileObj) {
fileObj.metadata = {
uploadedBy: Meteor.userId()
};
}
})
],
filter: {
allow: {
contentTypes: ['image/*']
}
}
});
And for the publication, you can use reywood:publish-composite
Meteor.publishComposite('photosWithUsers', function() {
return {
find: function() {
return Photos.find();
},
children: [
{
find: function(photo) {
return Meteor.users.find(photo.uploadedBy, {
fields: {username: 1, 'profile.name': 1}
});
}
}
]
};
});
Of course on the client, you need to subscribe to the photosWithUsers publication.
Now to access that information in the client, since you cannot apply a transform or a helper on the collectionFS documents, you can create a global template helper:
Template.registerHelper('getUsername', function(userId) {
check(userId, String);
var user = Meteor.users.findOne(userId);
return user && user.profile.name + ' (' + user.username + ')';
});
Now you can use that helper in your templates:
<template name="somePhoto">
{{#with FS.GetFile "Photos" photo}}
<img src="{{url}}" alt="This photo has been uploaded by {{getUsername uploadedBy}}">
{{/with}}
</template>
Template.somePhoto.helpers({
photo: function() {
return Photos.findOne();
}
})
Related
How to publish single objects seems not clear enough to me. Please what's the best way to handle this. This code snippet does not display anything on the view.
Helper file
singleSchool: function () {
if (Meteor.userId()) {
let myslug = FlowRouter.getParam('myslug');
var subValues = Meteor.subscribe('SingleSchool', myslug );
if (myslug ) {
let Schools = SchoolDb.findOne({slug: myslug});
if (Schools && subValues.ready()) {
return Schools;
}
}
}
},
Publish file
Meteor.publish('SingleSchool', function (schoolSlug) {
check( schoolSlug, Match.OneOf( String, null, undefined ) );
user = Meteor.users.findOne({_id:this.userId})
if(user) {
if(user.emails[0].verified) {
return SchoolDb.findOne({ slug: schoolSlug, userId: {$lt: this.userId}});
} else {
throw new Meteor.Error('Not authorized');
return false;
}
}
});
template file
<template name="view">
{{#if currentUser}}
{{#if Template.subscriptionsReady }}
{{#with singleSchool}}
{{singleSchool._id}}
{{singleSchool.addschoolname}}
{{/with}}
{{/if}}
{{/if}}
</template>
As you said "This code snippet does not display anything on the view." well, inside Meteor.publish you need to return cursor, not array or any other object.
So use this code:
Meteor.publish('SingleSchool', function (schoolSlug) {
check( schoolSlug, Match.OneOf( String, null, undefined ) );
var user = Meteor.users.findOne({_id:this.userId});
if(!user || !user.emails[0].verified) {
throw new Meteor.Error('Not authorized');
}
return SchoolDb.find({ slug: schoolSlug, userId: {$lt: this.userId}},{limit:1});
});
I would definitely recommend you to go through How to avoid Common Mistakes
When I am concerned only for a single object, I implement this using a meteor method:
Meteor.methods({
"getSingleSchool":function(schoolSlug) {
//... check args and user permissions
return SchoolDb.findOne({ slug: schoolSlug, userId: {$lt: this.userId}});
},
});
Then in the template I run this method in the onCreated autorun part:
Template.view.onCreated(function(){
const instance = this;
instance.state = new ReactiveDict();
instance.autorun(function(){
let my slug = FlowRouter.getParam('myslug');
// load if no loaded yet
if (my slug && !instance.state.get("singleSchool")) {
Meteor.call("getSingleSchool", mySlug, function(err, res){
//handle err if occurred...
this.state.set("singleSchool", res);
}.bind(instance)); //make instance available
}
});
});
The helper then just returns a value, if the school is loaded:
singleSchool: function () {
return Template.instance().state.get("singleSchool");
},
I have the following route :
this.route('groupPage', {
path: '/group/:_groupId',
waitOn: function(){
return Meteor.subscribe("groupPage", this.params._groupId);
},
data: function() {
var group = Groups.findOne({_id: this.params._groupId});
var members = Meteor.users.find({_id : {$in: group.memberIds}}); ******** ISSUE HERE******
return {
group: group,
members: members,
}; }});
and the following publication :
Meteor.publishComposite('groupPage', function(groupId, sortOrder, limit) {
return {
// return the group
find: function() {
if(this.userId){
var selector = {_id: groupId};
var options = {limit: 1};
return Groups.find(selector, options);
}
else{
return ;
}
},
children: [
{ // return the members
find: function(group) {
var selector = {_id: {$in: group.memberIds} };
return Meteor.users.find(selector);
}
}
]}}) ;
Now my issue is that : when the related page renders for the first there is no problems but when i actualize the group Page view the line : var members = Meteor.users.find({_id : {$in: group.memberIds}}); gives me the error : undefined object don't have memberIds property. i guess it's because the subscription is not yet ready when doing group.memberIds , isn't it ? Please a hint.
Thanks.
The data function doesn't wait for the subscription to be ready. Further more, subscriptions in the router are considered an anti-pattern for the most part, and should be done in the template: https://www.discovermeteor.com/blog/template-level-subscriptions/
I would pass to the template the groupId, and then get the group and members in the template, like so:
this.route('groupPage', {
path: '/group/:_groupId',
data: function() {
return {
_groupId: this.params._groupId,
}
}
});
and then in the template file:
Template.groupPage.onCreated(function(){
this.subscribe("groupPage", this.data._groupId);
})
Template.groupPage.helpers({
members(function(){
tempInst = Template.instance()
var group = Groups.findOne({_id: tempInst.data._groupId});
return Meteor.users.find({_id : {$in: group.memberIds}});
})
})
The general pattern of your route and publication are all solid. I suspect it's something simple such as:
There is no group with the _id you're using
You're not logged in when you load the route
Here's a version of your code that guards against the error. Note that the publication executes this.ready() instead of just returning if the user is not logged in.
this.route('groupPage', {
path: '/group/:_groupId',
waitOn: function(){
return Meteor.subscribe("groupPage", this.params._groupId);
},
data: function() {
var group = Groups.findOne({_id: this.params._groupId});
var members = group && Meteor.users.find({_id : {$in: group.memberIds}});
return { group: group, members: members };
}
});
Meteor.publishComposite('groupPage', function(groupId,sortOrder,limit) {
return {
find: function() {
if (this.userId) return Groups.find(groupId);
this.ready()
}
},
children: [
find: function(group) {
var selector = {_id: {$in: group.memberIds} };
return Meteor.users.find(selector);
}
]
});
I'm struggling to implement reactive tables based on a FS.Collection object. I've tried both aldeed/meteor-tabular and aslagle/reactive-table but both fail because the collection doesn't appear to exist. However, if I subscribe and retrieve fields from the Collection without using a reactive table package then the data displays just fine. What am I missing? It can't be a coincidence that both packages fail to work...
Here's my implementation with the aslagle/reactive-table package...
//template
<template name="documentTable">
{{#if Template.subscriptionsReady}}
{{> reactiveTable settings=settings}}
{{else}}
{{> spinner}}
{{/if}}
{{#if currentUser}}
{{> fileUpload}}
{{/if}}
</template>
//documents js
Template.documents.onCreated( () => {
p_id = FlowRouter.current().params.id;
Template.instance().subscribe('documents', p_id);
});
Template.documents.helpers({
documents: function () {
return Documents.find();
},
settings: function () {
return {
collection: documents,
showFilter: false,
rowsPerPage: 5,
showNavigation: auto,
showRowCount: true,
fields: ['_id','userId','propertyId','uploadedAt']
};
}
});
//collection definition
if (Meteor.isServer) {
var docStore = new FS.Store.S3("documents", {
region: "eu-west-1",
accessKeyId: (Meteor.isServer && !process.env.AWS_ACCESS_KEY_ID ? Meteor.settings.AWSAccessKeyId : null),
secretAccessKey: (Meteor.isServer && !process.env.AWS_SECRET_ACCESS_KEY ? Meteor.settings.AWSSecretAccessKey : null),
bucket: Meteor.isServer && process.env.AWS_S3_BUCKET || Meteor.settings.AWSBucket,
folder: "documents"
});
Documents = new FS.Collection("Documents", {
stores: [docStore],
filter: {
allow: {
contentTypes: ['application/pdf']
}
}
});
}
// end server
if (Meteor.isClient) {
var docStore = new FS.Store.S3("documents");
Documents = new FS.Collection("Documents", {
stores: [docStore],
filter: {
allow: {
contentTypes: ['application/pdf']
}
}
});
}
// end client
// allow rules
Documents.allow({
insert: function(userId) {
// only allow insert from signed in user
return userId != null;
},
update: function(userId) {
// only allow update from signed in uesr
return userId != null;
},
download: function() {
return true;
},
});
In the reactive-table case I'm getting the error that the argument is not an instance of Mongo.Collection, a cursor or an array while with meteor-tabular it fails to start because it encounters a ReferenceError and states that Documents isn't defined.
Anyone any thoughts on this?
I'm using aslagle/reactive-table with mongo quite well, with a pub/sub model; I don't know what your new FS is? is that a mongo collection?
I have something like this when I use the reactive-table...
//on the server
Documents = new Mongo.Collection('documents');
//on the client js
Documents = new Mongo.Collection('documents');
Template.documents.helpers({
settings: function () {
return {
collection: Documents.find(),
rowsPerPage: 5,
showFilter: false,
showNavigation: auto,
showRowCount: true,
fields: [
{key: '_id',
label: 'ID' },
{key: 'userId',
label: 'User#' },
{key: 'propertyId',
label: 'Property#' },
{key: 'uploadedAt',
label: 'Uploaded' },
]
};
}
});
//on the client html file
{{> reactiveTable class="table table-bordered table-hover" settings=settings}}
I'm running into the same problem as issue #53 of aldeed:tabular. When defining the table as suggested in the documentation, it is too soon to invoke a translation function (TAPi18n.__ or other), since the I18N variables are not yet set.
What is the nice, reactive way of feeding the translated column titles into DataTables, either directly as suggested by aldeed himself upon closing the issue, or through aldeed:tabular?
With .tabular.options
There is a way with the template's .tabular.options reactive
variable, but it is quirky. Here is a variation of the library
example using
tap-i18n to translate the
column headers:
function __(key) {
if (Meteor.isServer) {
return key;
} else {
return TAPi18n.__(key);
}
}
Books = new Meteor.Collection("Books");
TabularTables = {};
TabularTables.Books = new Tabular.Table({
name: "Books",
collection: Books,
columns: [] // Initially empty, reactively updated below
});
var getTranslatedColumns = function() {
return [
{data: "title", title: __("Title")},
{data: "author", title: __("Author")},
{data: "copies", title: __("Copies Available")},
{
data: "lastCheckedOut",
title: __("Last Checkout"),
render: function (val, type, doc) {
if (val instanceof Date) {
return moment(val).calendar();
} else {
return "Never";
}
}
},
{data: "summary", title: __("Summary")},
{
tmpl: Meteor.isClient && Template.bookCheckOutCell
}
];
}
if (Meteor.isClient) {
Template.tabular.onRendered(function() {
var self = this;
self.autorun(function() {
var options = _.clone(self.tabular.options.get());
options.columns = getTranslatedColumns();
self.tabular.options.set(_.clone(options));
});
});
}
With a forked version
I created a pull request against branch devel of meteor-tabular to enable the straightforward, reactive-based approach like so:
<template name="MyTemplateWithATable">
{{> tabular table=makeTable class="table table-editable table-striped table-bordered table-condensed"}}
</template>
var MyColumns = ["title", "author"];
// Assume translations are set up for "MyTable.column.title", "MyTable.column.author"
// in other source files; see TAPi18n documentation for how to do that
function makeTable() {
return new Tabular.Table({
name: "MyTable",
collection: MyCollection,
columns: _.map(MyColumns,
function(colSymbol) {
return {
data: colSymbol,
title: TAPi18n.__("MyTable.column." + colSymbol)
};
})
});
}
if (Meteor.isServer) {
// Called only once
makeTable();
} else if (Meteor.isClient) {
// Reactively called multiple times e.g. when switching languages
Template.MyTemplateWithATable.helpers({makeTable: makeTable});
}
Recent versions of aldeed:tabular allow to specify a function for setting the column titles.
import {TAPi18n} from 'meteor/tap:i18n';
TabularTables = {};
TabularTables.Departments= new Tabular.Table({
name: 'Departments',
collection: Departments,
responsive: true,
autoWidth: true,
stateSave: false,
columns: [
{data: "name", titleFn: function() {
return TAPi18n.__("name");
}},
{data: "description", titleFn: function() {
return TAPi18n.__("description");
}}
]
});
The language change is reactive. If you have translations you can switch and columns will be translated.
TAPi18n.setLanguage("en");
TAPi18n.setLanguage("de");
Word of warning:
This currently does not work when you include invisible columns in your table data. The offset is wrong and you get wrong column titles.
So I have a route that sets my template
Router.route('audit', {
path: '/audit/:audit_id/',
template: 'audit',
data: function() {
if (this.ready()) {
audit_obj = Audits.findOne({_id: this.params.audit_id});
lineitems = LineItems.find(JSON.parse(audit.query));
return {
audit_obj: audit_obj,
lineitems: lineitems
}
}
},
waitOn: function () {
return [
Meteor.subscribe('lineitems', this.params.audit_id),
Meteor.subscribe('audits')
]
}
}
Now, when my user takes certain actions on the page rendered by the audit template, I would like to update the audit object and also update the data context that the page is running with. Is this possible?
Something like:
Template.audit.events({
'click .something-button': function() {
// update the data context for the current audit template.
current_context.audit_obj.something = 'new something';
}
});
Yes:
Router.route('audit', {
path: '/audit/:audit_id/',
template: 'audit',
onRun: function() {
Session.set('audit', Audits.findOne(this.params.audit_id));
Session.set('lineitems', LineItems.find(JSON.parse(audit.query)).fetch());
}
data: function() {
if (this.ready()) {
return {
audit_obj: Session.get('audit'),
lineitems: Session.get('lineitems')
}
}
},
waitOn: function () {
return [
Meteor.subscribe('lineitems', this.params.audit_id),
Meteor.subscribe('audits')
]
}
}
and
Template.audit.events({
'click .something-button': function() {
// update the data context for the current audit template.
Session.set('audit', {..});
}
});
But you'll need to decide how to handle changes that come from the server, and may interfere with changes on the front end. So a better approach might be to leave the first part of the code (router) as is:
Router.route('audit', {
path: '/audit/:audit_id/',
template: 'audit',
data: function() {
if (this.ready()) {
return {
audit_obj: Audits.findOne(this.params.audit_id),
lineitems: LineItems.find(JSON.parse(audit.query))
}
}
},
waitOn: function () {
return [
Meteor.subscribe('lineitems', this.params.audit_id),
Meteor.subscribe('audits')
]
}
}
and just change the front end to update the collection:
Template.audit.events({
'click .something-button': function() {
// update the data context for the current audit template.
Audits.update( this.data.audit_obj._id, {..} );
}
});
Of course, that will update the data on the server, too.