I'm using keystonejs and CloudinaryImages to create an Image Gallery.
{ type: Types.CloudinaryImages }
I need the ability to add a caption to the images.
I was also reading this:
https://github.com/keystonejs/keystone/pull/604
but I could not figure out if this option is already in place or not.
Any idea?
Thanks.
I had a similar problem, I wanted to be able to give Images there own descriptions and other attributes, while also being included in a Gallery with a Gallery description.
This may be more than you are looking for but here is a Image model:
var keystone = require('keystone'),
Types = keystone.Field.Types;
/**
* Image Model
* ==================
*/
var Image = new keystone.List('Image', {
map: { name: 'name' },
autokey: { path: 'slug', from: 'name', unique: true }
});
Image.add({
name: { type: String, required: true },
image: { type: Types.CloudinaryImage, autoCleanup: true, required: true, initial: false },
description: { type: Types.Textarea, height: 150 },
});
Image.relationship({ ref: 'Gallery', path: 'heroImage' });
Image.relationship({ ref: 'Gallery', path: 'images' });
Image.register();
And the Galleries that contain these images looks like this:
var keystone = require('keystone'),
Types = keystone.Field.Types;
/**
* Gallery Model
* =============
*/
var Gallery = new keystone.List('Gallery', {
map: { name: 'name' },
autokey: { path: 'slug', from: 'name', unique: true }
});
Gallery.add({
name: { type: String, required: true},
published: {type: Types.Select, options: 'yes, no', default: 'no', index: true, emptyOption: false},
publishedDate: { type: Types.Date, index: true, dependsOn: { published: 'yes' } },
description: { type: Types.Textarea, height: 150 },
heroImage : { type: Types.Relationship, ref: 'Image' },
images : { type: Types.Relationship, ref: 'Image', many: true }
});
Gallery.defaultColumns = 'title, published|20%, publishedDate|20%';
Gallery.register();
You will need to create Template Views and Routes to Handle this, but it isn't too much more work - these are just the Models - let me know if you would like me to post the routes I am using for this, I am using Handlebars for my views so that may not be as helpful.
Related
I have two simple modes:
PresentationType:
var keystone = require('keystone');
var PresentationType = new keystone.List('PresentationType', {
autokey: { from: 'name', path: 'key', unique: true },
});
PresentationType.add({
name: { type: String, required: true },
t1: { type: Boolean },
t2: { type: Boolean },
});
PresentationType.relationship({ ref: 'StaticPage', path: 'pages', refPath: 'presentationType' });
PresentationType.register();
Static Page:
var keystone = require('keystone');
var Types = keystone.Field.Types;
var StaticPage = new keystone.List('StaticPage', {
map: { name: 'title' },
autokey: { path: 'slug', from: 'title', unique: true },
drilldown: 'presentationType',
});
StaticPage.add({
title: { type: String, required: true },
presentationType: { type: Types.Relationship, ref: 'PresentationType', many: false },
text1: { type: String, dependsOn: { presentationType.t1: true } },
text2: { type: String, dependsOn: { presentationType.t2: true } },
});
StaticPage.defaultColumns = 'title';
StaticPage.register();
First i create a presentation type that has boolean attributes, text1 and text2
Secondly when i create a page and specify it's presentation type, i want to be able to display certain fields based on the presentation type boolean.
So far i cant seem to find an answer to it.
The dependsOn attribute cannot be used across a relationship field; that field would constantly need to be populated with that relationship. dependsOn within a model can only be used within other static fields of the same model (and not across different models.)
http://keystonejs.com/docs/database/#fields-conditional
I'm trying to make Keystone into a CMS. So, I need models for Article, Category, ImagePage, AttachmentPage and so on. Every model I mentioned has a subset of common fields like: title, content, meta: {title, description, keywords} and so on.
In Keystone a model is constructed like this:
Article.add(fieldsCollectionObject)
so I defined the common fields in external file:
var T = require('keystone').Field.Types;
module.exports = {
title: { type: T.Text, required: true },
content: { type: T.Html, wysiwyg: true, height: 400 },
meta: {
title: { type: T.Text },
desc: { type: T.Textarea, height: 50 },
keywords: { type: T.Text },
},
publishedDate: { type: T.Date, index: true, dependsOn: { state: 'published' } },
state: { type: T.Select, options: 'draft, published, archived', default: 'draft', index: true },
};
and having require'd it in model's file I do:
const _ = require('lodash');
const pageDef = require('./common/Page.js');
const keystone = require('keystone');
const T = keystone.Field.Types;
<...>
Article.add(_.defaultsDeep({
brief: { type: T.Html, wysiwyg: true, height: 150 },
category: { type: T.Relationship, ref: 'Category', many: false, collapse: true },
tags: { type: T.Relationship, ref: 'Tag', many: true },
}, defs.authored, pageDef));
Now, the problem is with the order of fields in the Admin UI - unsurprisingly the brief, category and tags go before fields from pageDef. Is there any way to impose an order I want? Like title, brief, content, <the rest>?
defaults and defaultsDeep mutate the first object passed as a parameter to it (your initial object of Keystone fields). To have your own order, you would need to pass the objects to _.defaultsDeep in the order that you want them to appear in the object, and hence the order that they appear in the Admin UI.
Helpfully, duplicate items will not be included in the resulting object. So you would have something like this:
const _ = require('lodash');
const pageDef = require('./common/Page.js');
const keystone = require('keystone');
const T = keystone.Field.Types;
//....
let articleDef = {
brief: { type: T.Html, wysiwyg: true, height: 150 },
category: { type: T.Relationship, ref: 'Category', many: false, collapse: true },
tags: { type: T.Relationship, ref: 'Tag', many: true };
};
Article.add(_.defaultsDeep({
title: pageDef.title,
brief: articleDef.brief,
content: pageDef.content},
pageDef, articleDef));
The answer above turned out to be way to go. So I expanded and built upon it:
lib/util.js
const _ = require('lodash');
class Util {
static sourceFields (fields, ...sources) {
const source = _.defaultsDeep(...sources);
const result = [];
for (let fieldSet of fields) {
result.push(_.isArray(fieldSet) ? _.pick(source, fieldSet) : fieldSet);
}
return result;
}
}
module.exports = Util;
models/common/traits.js
var T = require('keystone').Field.Types;
module.exports = {
title: { type: T.Text, required: true },
content: { type: T.Html, wysiwyg: true, height: 400 },
indexImage: { type: T.CloudinaryImage },
meta: {
title: { type: T.Text },
desc: { type: T.Textarea, height: 50 },
keywords: { type: T.Text },
},
// <...>
}
models/Article.js
const util = require('../lib/utils.js');
const defs = require('./common/traits.js');
const keystone = require('keystone');
const T = keystone.Field.Types;
// < var Article declaration... >
const ownDef = {
brief: { type: T.Html, wysiwyg: true, height: 150 },
category: { type: T.Relationship, ref: 'Category', many: false, collapse: true },
tags: { type: T.Relationship, ref: 'Tag', many: true },
images: { type: T.Relationship, ref: 'Image', many: true, collapse: true },
attachments: { type: T.Relationship, ref: 'Attachment', many: true, collapse: true },
};
Article.add(...util.sourceFields([
'Content', ['title', 'brief', 'content', 'indexImage', 'category', 'tags'],
'Media', ['images', 'attachments'],
'SEO', ['meta'],
'Status', ['pageType', 'author', 'state', 'publishedDate'],
], ownDef, defs));
So, in traits.js I define common fields, in Article.js - fields I use only in Article model. Then, in Article model I add the fields to the List with the help of sourceFields() function. sourceFields() gets an array of fieldsets and unspecified number of field definition objects (like ownDef and defs).
The fieldset is either a string or an array of field names (keys in definition objects). If it's string it'll be a header in Admin UI, if it's array then it'll be a set of fields ordered just like field names in the array - the function basically inserts field definition into a "slot" specified in fieldset.
I'm looking for a tag field type which will autocomplete if the tag already exists, or simply add the tag if it doesn't. I think there are a lot of implementations of this in other CMS' and I wanted to shake the tree to see if someone had already done this before I roll up my sleeves. Assuming it existed, I imagine it would be implemented as follows:
var keystone = require('keystone'),
Types = keystone.Field.Types;
var Verbiage = new keystone.List('Verbiage', {
autokey: { path: 'slug', from: 'title', unique: true },
map: { name: 'title' },
defaultSort: '-createdAt',
label: "Verbiage",
plural : "Verbiage"
});
Verbiage.add({
title: { type: String, required: true },
author: { type: Types.Relationship, ref: 'User' },
tagged: { type: Types.Tag, required: false, many: true },
createdAt: { type: Date, default: Date.now },
publishedAt: Date
});
Verbiage.register();
So I have a Post model (built in of course). Each Post has an Author. Each Author has a Profile (I built this out separately deliberately as I am trying to learn. I am having real problems populating the profile
Here's what I have in my blog.js
// Load the posts
view.on('init', function(next) {
var q = keystone.list('Post').paginate({
page: req.query.page || 1,
perPage: 10,
maxPages: 10
})
.where('state', 'published')
.sort('-publishedDate')
.populate('categories')
.populate('author');
if (locals.data.category) {
q.where('categories').in([locals.data.category]);
}
q.exec(function(err, results) {
locals.data.posts = results;
async.each(results.results, function(post,_next) {
post.author.populate('profile', function(err, author) {
post.author = author;
locals.data.posts.results.push(post);
_next();
});
}, function(err) {
next();
})
})
});
Here is Profile
var keystone = require('keystone'),
Types = keystone.Field.Types;
var Profile = new keystone.List('Profile', {
map: { name: 'nickname' },
autokey: { path: 'slug', from: 'nickname', unique: true }
});
Profile.add({
nickname: { type: String, required: true, initial: true},
age: { type: Types.Number, initial: false, required: false, index: true },
location: { type: Types.Text, initial: false, required: false, index: true }
});
Profile.defaultColumns = 'nickname, age, location';
Profile.register();
Here is user
var keystone = require('keystone'),
Types = keystone.Field.Types;
/**
* User Model
* ==========
*/
var User = new keystone.List('User');
User.add({
name: { type: Types.Name, required: true, index: true },
email: { type: Types.Email, initial: true, required: true, index: true },
password: { type: Types.Password, initial: true, required: true },
profile: { type: Types.Relationship, ref: 'Profile', index: true },
}, 'Permissions', {
isAdmin: { type: Boolean, label: 'Can access Keystone', index: true }
});
// Provide access to Keystone
User.schema.virtual('canAccessKeystone').get(function() {
return this.isAdmin;
});
/**
* Relationships
*/
User.relationship({ ref: 'Post', path: 'posts', refPath: 'author' });
/**
* Registration
*/
User.defaultColumns = 'name, email, isAdmin';
User.register();
Here is Post
var keystone = require('keystone'),
Types = keystone.Field.Types;
/**
* Post Model
* ==========
*/
var Post = new keystone.List('Post', {
map: { name: 'title' },
autokey: { path: 'slug', from: 'title', unique: true }
});
Post.add({
title: { type: String, required: true },
state: { type: Types.Select, options: 'draft, published, archived', default: 'draft', index: true },
author: { type: Types.Relationship, ref: 'User', index: true },
publishedDate: { type: Types.Date, index: true, dependsOn: { state: 'published' } },
image: { type: Types.CloudinaryImage },
content: {
brief: { type: Types.Html, wysiwyg: true, height: 150 },
extended: { type: Types.Html, wysiwyg: true, height: 400 }
},
categories: { type: Types.Relationship, ref: 'PostCategory', many: true }
});
Post.schema.virtual('content.full').get(function() {
return this.content.extended || this.content.brief;
});
Post.defaultColumns = 'title, state|20%, author|20%, publishedDate|20%';
Post.register();
Thanks in advance!
I got the 'createdBy' field added to the model but it allows the admin to select all users as the 'createdBy' user. I want this field to be auto populated with the admin that is currently logged in and can't seem to get it to work.
Ideally this wouldn't appear in the UI at all but just be stored when the user is saved.
User.add({
name: { type: Types.Name, required: true, index: true },
email: { type: Types.Email, initial: true, required: true, index: true },
company: { type: String, required: true, index: true, initial: true },
phone: { type: String, required: true, index: true, initial: true },
password: { type: Types.Password, initial: true, required: true },
createdBy: { type: Types.Relationship, initial:true, required:true, ref: 'User' },
createdAt: { type: Date, default: Date.now }
}, 'Permissions', {
level : { type: Types.Select, numeric: true, options: [{ value: 1, label: 'User' }, { value: 2, label: 'Group Administrator' }, { value: 3, label: 'System Administrator' }] }
},
'Screening', {
rooms : { type: Types.Select, numeric: true, options: [{ value: 1, label: 'Screening room 1' }, { value: 2, label: 'Screening room 2' }, { value: 3, label: 'Screening room 3' }] }
});
While your implementation is functional, a number of Keystone developers (myself included) have raised concerns regarding the security risk of sending the user.id via a POST. You are also correct when you say that currently there is no good way of doing this in Keystone.
My solution was to implement the feature on Keystone itself. I added an optional meta pattern, which I called audit meta. This adds two fields to the List (createdBy and updatedBy) which I populate in the UpdateHandler() using an existing cached copy of req.user. This way there's no need to send user._id via POST.
To use it you just add List.addPattern('audit meta'); after defining your list, just like you would if you were using the standard meta. My implementation of audit meta also adds the standard meta fields, so there's no need to use both.
To implement this I made the following changes to Keystone
First, in lib\list.js I added the following code (prefixed with +) to the addPatern() method:
List.prototype.addPattern = function(pattern) {
switch (pattern) {
...
+ case 'audit meta':
+ var userModel = keystone.get('user model');
+
+ if(!this.schema.path('createdOn') && !this.schema.path('updatedOn')) {
+ this.addPattern('standard meta');
+ }
+
+ if (userModel) {
+ this.add({
+ createdBy: { type: Field.Types.Relationship, ref: userModel, hidden: true, index: true },
+ updatedBy: { type: Field.Types.Relationship, ref: userModel, hidden: true, index: true }
+ });
+ this.map('createdBy', 'createdBy');
+ this.map('modifiedBy', 'updatedBy');
+ }
+ break;
+
}
return this;
Then in lib/updateHandler.js I added the following code to UpdateHandler.prototype.process(), just before progress() is called at then end of the method.
+ // check for audit meta fields (mapped to createdBy/modifiedBy)
+ if (this.list.mappings.createdBy && this.item.isNew) {
+ this.item.set(this.list.mappings.createdBy, this.user._id);
+ }
+ if (this.list.mappings.modifiedBy) {
+ this.item.set(this.list.mappings.modifiedBy, this.user._id);
+ }
Earlier I submitted a pull request (https://github.com/JedWatson/keystone/pull/490) to Keystone, which includes a detailed explanation of my implementation. So, if you need this urgently, you can always fork a copy of Keystone and merge my PR.
Apparently there is no good way to do this but I did come up with a work around using someone else's idea by creating a custom hidden input field. It's not ideal but will work for this project. The default value on createdBy is just so I can make it a required field but it is populated in the form jade template not he initial jade template for that input type.
User.add({
name: { type: Types.Name, required: true, index: true },
email: { type: Types.Email, initial: true, required: true, index: true },
company: { type: String, required: true, index: true, initial: true },
phone: { type: String, required: true, index: true, initial: true },
password: { type: Types.Password, initial: true, required: true },
createdBy: { type: Types.Admin, required: true, initial: true, default: 'createdBy' },
createdAt: { type: Types.Hidden, default: Date.now }
}, 'Permissions', {
level : { type: Types.Select, numeric: true, options: [{ value: 1, label: 'User' }, { value: 2, label: 'Group Administrator' }, { value: 3, label: 'System Administrator' }] }
},Screening', {
rooms : { type: Types.Select, numeric: true, options: [{ value: 1, label: 'Screening room 1' }, { value: 2, label: 'Screening room 2' }, { value: 3, label: 'Screening room 3' }] }
});
then the custom fieldtype just something like this, just create one for form and initial. Also create the fieldTypes/admin.js and update the fieldTypes.index.js
Input admin/form.jade
.field(class='type-' + field.type, data-field-type=field.type, data-field-path=field.path, data-field-collapse=field.collapse ? 'true' : false, data-field-depends-on=field.dependsOn, data-field-noedit=field.noedit ? 'true' : 'false')
- var value = field.format(item)
.field-ui(class='width-' + field.width)
if field.noedit
.field-value= user._id
else
input(type='hidden', name=field.path, value=user._id, autocomplete='off').form-control
if field.note
.field-note!= field.note