FEATURE: Can edit category/host relationships for embedding

This commit is contained in:
Robin Ward 2015-08-18 17:15:46 -04:00
parent 913c3d6f63
commit d1c69189f3
36 changed files with 449 additions and 127 deletions

View File

@ -0,0 +1,7 @@
import RestAdapter from 'discourse/adapters/rest';
export default RestAdapter.extend({
pathFor() {
return "/admin/customize/embedding";
}
});

View File

@ -0,0 +1,63 @@
import { bufferedProperty } from 'discourse/mixins/buffered-content';
import computed from 'ember-addons/ember-computed-decorators';
import { on, observes } from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Component.extend(bufferedProperty('host'), {
editToggled: false,
tagName: 'tr',
categoryId: null,
editing: Ember.computed.or('host.isNew', 'editToggled'),
@on('didInsertElement')
@observes('editing')
_focusOnInput() {
Ember.run.schedule('afterRender', () => { this.$('.host-name').focus(); });
},
@computed('buffered.host', 'host.isSaving')
cantSave(host, isSaving) {
return isSaving || Ember.isEmpty(host);
},
actions: {
edit() {
this.set('categoryId', this.get('host.category.id'));
this.set('editToggled', true);
},
save() {
if (this.get('cantSave')) { return; }
const props = this.get('buffered').getProperties('host');
props.category_id = this.get('categoryId');
const host = this.get('host');
host.save(props).then(() => {
host.set('category', Discourse.Category.findById(this.get('categoryId')));
this.set('editToggled', false);
}).catch(popupAjaxError);
},
delete() {
bootbox.confirm(I18n.t('admin.embedding.confirm_delete'), (result) => {
if (result) {
this.get('host').destroyRecord().then(() => {
this.sendAction('deleteHost', this.get('host'));
});
}
});
},
cancel() {
const host = this.get('host');
if (host.get('isNew')) {
this.sendAction('deleteHost', host);
} else {
this.rollbackBuffer();
this.set('editToggled', false);
}
}
}
});

View File

@ -0,0 +1,18 @@
export default Ember.Controller.extend({
embedding: null,
actions: {
saveChanges() {
this.get('embedding').update({});
},
addHost() {
const host = this.store.createRecord('embeddable-host');
this.get('embedding.embeddable_hosts').pushObject(host);
},
deleteHost(host) {
this.get('embedding.embeddable_hosts').removeObject(host);
}
}
});

View File

@ -0,0 +1,9 @@
export default Ember.Route.extend({
model() {
return this.store.find('embedding');
},
setupController(controller, model) {
controller.set('embedding', model);
}
});

View File

@ -27,6 +27,7 @@ export default {
this.resource('adminUserFields', { path: '/user_fields' });
this.resource('adminEmojis', { path: '/emojis' });
this.resource('adminPermalinks', { path: '/permalinks' });
this.resource('adminEmbedding', { path: '/embedding' });
});
this.route('api');

View File

@ -0,0 +1,19 @@
{{#if editing}}
<td>
{{input value=buffered.host placeholder="example.com" enter="save" class="host-name"}}
</td>
<td>
{{category-chooser value=categoryId}}
</td>
<td>
{{d-button icon="check" action="save" class="btn-primary" disabled=cantSave}}
{{d-button icon="times" action="cancel" class="btn-danger" disabled=host.isSaving}}
</td>
{{else}}
<td>{{host.host}}</td>
<td>{{category-badge host.category}}</td>
<td>
{{d-button icon="pencil" action="edit"}}
{{d-button icon="trash-o" action="delete" class='btn-danger'}}
</td>
{{/if}}

View File

@ -5,6 +5,7 @@
{{nav-item route='adminUserFields' label='admin.user_fields.title'}}
{{nav-item route='adminEmojis' label='admin.emoji.title'}}
{{nav-item route='adminPermalinks' label='admin.permalink.title'}}
{{nav-item route='adminEmbedding' label='admin.embedding.title'}}
{{/admin-nav}}
<div class="admin-container">

View File

@ -0,0 +1,15 @@
{{#if embedding.embeddable_hosts}}
<table>
<tr>
<th style='width: 50%'>{{i18n "admin.embedding.host"}}</th>
<th style='width: 30%'>{{i18n "admin.embedding.category"}}</th>
<th style='width: 20%'>&nbsp;</th>
</tr>
{{#each embedding.embeddable_hosts as |host|}}
{{embeddable-host host=host deleteHost="deleteHost"}}
{{/each}}
</table>
{{/if}}
{{d-button label="admin.embedding.add_host" action="addHost" icon="plus" class="btn-primary"}}

View File

@ -1,4 +1,4 @@
const ADMIN_MODELS = ['plugin', 'site-customization'];
const ADMIN_MODELS = ['plugin', 'site-customization', 'embeddable-host'];
export function Result(payload, responseJson) {
this.payload = payload;
@ -19,7 +19,7 @@ function rethrow(error) {
export default Ember.Object.extend({
basePath(store, type) {
if (ADMIN_MODELS.indexOf(type) !== -1) { return "/admin/"; }
if (ADMIN_MODELS.indexOf(type.replace('_', '-')) !== -1) { return "/admin/"; }
return "/";
},

View File

@ -189,15 +189,25 @@ export default Ember.Object.extend({
_hydrateEmbedded(type, obj, root) {
const self = this;
Object.keys(obj).forEach(function(k) {
const m = /(.+)\_id$/.exec(k);
const m = /(.+)\_id(s?)$/.exec(k);
if (m) {
const subType = m[1];
if (m[2]) {
const hydrated = obj[k].map(function(id) {
return self._lookupSubType(subType, type, id, root);
});
obj[self.pluralize(subType)] = hydrated || [];
delete obj[k];
} else {
const hydrated = self._lookupSubType(subType, type, obj[k], root);
if (hydrated) {
obj[subType] = hydrated;
delete obj[k];
}
}
}
});
},

View File

@ -0,0 +1,34 @@
class Admin::EmbeddableHostsController < Admin::AdminController
before_filter :ensure_logged_in, :ensure_staff
def create
save_host(EmbeddableHost.new)
end
def update
host = EmbeddableHost.where(id: params[:id]).first
save_host(host)
end
def destroy
host = EmbeddableHost.where(id: params[:id]).first
host.destroy
render json: success_json
end
protected
def save_host(host)
host.host = params[:embeddable_host][:host]
host.category_id = params[:embeddable_host][:category_id]
host.category_id = SiteSetting.uncategorized_category_id if host.category_id.blank?
if host.save
render_serialized(host, EmbeddableHostSerializer, root: 'embeddable_host', rest_serializer: true)
else
render_json_error(host)
end
end
end

View File

@ -0,0 +1,21 @@
class Admin::EmbeddingController < Admin::AdminController
before_filter :ensure_logged_in, :ensure_staff, :fetch_embedding
def show
render_serialized(@embedding, EmbeddingSerializer, root: 'embedding', rest_serializer: true)
end
def update
render_serialized(@embedding, EmbeddingSerializer, root: 'embedding', rest_serializer: true)
end
protected
def fetch_embedding
@embedding = OpenStruct.new({
id: 'default',
embeddable_hosts: EmbeddableHost.all.order(:host)
})
end
end

View File

@ -58,8 +58,7 @@ class EmbedController < ApplicationController
def ensure_embeddable
if !(Rails.env.development? && current_user.try(:admin?))
raise Discourse::InvalidAccess.new('embeddable hosts not set') if SiteSetting.embeddable_hosts.blank?
raise Discourse::InvalidAccess.new('invalid referer host') unless SiteSetting.allows_embeddable_host?(request.referer)
raise Discourse::InvalidAccess.new('invalid referer host') unless EmbeddableHost.host_allowed?(request.referer)
end
response.headers['X-Frame-Options'] = "ALLOWALL"

View File

@ -0,0 +1,24 @@
class EmbeddableHost < ActiveRecord::Base
validates_format_of :host, :with => /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?\Z/i
belongs_to :category
before_validation do
self.host.sub!(/^https?:\/\//, '')
self.host.sub!(/\/.*$/, '')
end
def self.record_for_host(host)
uri = URI(host) rescue nil
return false unless uri.present?
host = uri.host
return false unless host.present?
where("lower(host) = ?", host).first
end
def self.host_allowed?(host)
record_for_host(host).present?
end
end

View File

@ -68,20 +68,6 @@ class SiteSetting < ActiveRecord::Base
@anonymous_menu_items ||= Set.new Discourse.anonymous_filters.map(&:to_s)
end
def self.allows_embeddable_host?(host)
return false if embeddable_hosts.blank?
uri = URI(host) rescue nil
return false unless uri.present?
host = uri.host
return false unless host.present?
!!embeddable_hosts.split("\n").detect {|h| h.sub(/^https?\:\/\//, '') == host }
hosts = embeddable_hosts.split("\n").map {|h| (URI(h).host rescue nil) || h }
!!hosts.detect {|h| h == host}
end
def self.anonymous_homepage
top_menu_items.map { |item| item.name }
.select { |item| anonymous_menu_items.include?(item) }

View File

@ -866,7 +866,7 @@ class Topic < ActiveRecord::Base
end
def expandable_first_post?
SiteSetting.embeddable_hosts.present? && SiteSetting.embed_truncate? && has_topic_embed?
SiteSetting.embed_truncate? && has_topic_embed?
end
TIME_TO_FIRST_RESPONSE_SQL ||= <<-SQL

View File

@ -33,12 +33,14 @@ class TopicEmbed < ActiveRecord::Base
# If there is no embed, create a topic, post and the embed.
if embed.blank?
Topic.transaction do
eh = EmbeddableHost.record_for_host(url)
creator = PostCreator.new(user,
title: title,
raw: absolutize_urls(url, contents),
skip_validations: true,
cook_method: Post.cook_methods[:raw_html],
category: SiteSetting.embed_category)
category: eh.try(:category_id))
post = creator.create
if post.present?
TopicEmbed.create!(topic_id: post.topic_id,

View File

@ -0,0 +1,16 @@
class EmbeddableHostSerializer < ApplicationSerializer
attributes :id, :host, :category_id
def id
object.id
end
def host
object.host
end
def category_id
object.category_id
end
end

View File

@ -0,0 +1,8 @@
class EmbeddingSerializer < ApplicationSerializer
attributes :id
has_many :embeddable_hosts, serializer: EmbeddableHostSerializer, embed: :ids
def id
object.id
end
end

View File

@ -2491,6 +2491,14 @@ en:
image: "Image"
delete_confirm: "Are you sure you want to delete the :%{name}: emoji?"
embedding:
confirm_delete: "Are you sure you want to delete that host?"
title: "Embedding"
host: "Allowed Hosts"
edit: "edit"
category: "Post to Category"
add_host: "Add Host"
permalink:
title: "Permalinks"
url: "URL"

View File

@ -1164,13 +1164,11 @@ en:
autohighlight_all_code: "Force apply code highlighting to all preformatted code blocks even when they didn't explicitly specify the language."
highlighted_languages: "Included syntax highlighting rules. (Warning: including too many langauges may impact performance) see: https://highlightjs.org/static/demo/ for a demo"
embeddable_hosts: "Host(s) that can embed the comments from this Discourse forum. Hostname only, do not begin with http://"
feed_polling_enabled: "EMBEDDING ONLY: Whether to embed a RSS/ATOM feed as posts."
feed_polling_url: "EMBEDDING ONLY: URL of RSS/ATOM feed to embed."
embed_by_username: "Discourse username of the user who creates the embedded topics."
embed_username_key_from_feed: "Key to pull discourse username from feed."
embed_truncate: "Truncate the embedded posts."
embed_category: "Category of embedded topics."
embed_post_limit: "Maximum number of posts to embed."
embed_whitelist_selector: "CSS selector for elements that are allowed in embeds."
embed_blacklist_selector: "CSS selector for elements that are removed from embeds."

View File

@ -135,6 +135,8 @@ Discourse::Application.routes.draw do
get "customize/css_html/:id/:section" => "site_customizations#index", constraints: AdminConstraint.new
get "customize/colors" => "color_schemes#index", constraints: AdminConstraint.new
get "customize/permalinks" => "permalinks#index", constraints: AdminConstraint.new
get "customize/embedding" => "embedding#show", constraints: AdminConstraint.new
put "customize/embedding" => "embedding#update", constraints: AdminConstraint.new
get "flags" => "flags#index"
get "flags/:filter" => "flags#index"
post "flags/agree/:id" => "flags#agree"
@ -148,6 +150,7 @@ Discourse::Application.routes.draw do
resources :emojis, constraints: AdminConstraint.new
end
resources :embeddable_hosts, constraints: AdminConstraint.new
resources :color_schemes, constraints: AdminConstraint.new
resources :permalinks, constraints: AdminConstraint.new

View File

@ -759,16 +759,12 @@ developer:
default: false
embedding:
embeddable_hosts:
default: ''
type: host_list
feed_polling_enabled: false
feed_polling_url: ''
embed_by_username:
default: ''
type: username
embed_username_key_from_feed: ''
embed_category: ''
embed_post_limit: 100
embed_truncate: false
embed_whitelist_selector: ''

View File

@ -0,0 +1,33 @@
class CreateEmbeddableHosts < ActiveRecord::Migration
def change
create_table :embeddable_hosts, force: true do |t|
t.string :host, null: false
t.integer :category_id, null: false
t.timestamps
end
category_id = execute("SELECT c.id FROM categories AS c
INNER JOIN site_settings AS s ON s.value = c.name
WHERE s.name = 'embed_category'")[0]['id'].to_i
if category_id == 0
category_id = execute("SELECT value FROM site_settings WHERE name = 'uncategorized_category_id'")[0]['value'].to_i
end
embeddable_hosts = execute("SELECT value FROM site_settings WHERE name = 'embeddable_hosts'")
if embeddable_hosts && embeddable_hosts.cmd_tuples > 0
val = embeddable_hosts[0]['value']
if val.present?
records = val.split("\n")
if records.present?
records.each do |h|
execute "INSERT INTO embeddable_hosts (host, category_id, created_at, updated_at) VALUES ('#{h}', #{category_id}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
end
end
end
end
execute "DELETE FROM site_settings WHERE name IN ('embeddable_hosts', 'embed_category')"
end
end

View File

@ -13,7 +13,7 @@ class TopicRetriever
private
def invalid_host?
!SiteSetting.allows_embeddable_host?(@embed_url)
!EmbeddableHost.host_allowed?(@embed_url)
end
def retrieved_recently?

View File

@ -0,0 +1,9 @@
require 'spec_helper'
describe Admin::EmbeddableHostsController do
it "is a subclass of AdminController" do
expect(Admin::EmbeddableHostsController < Admin::AdminController).to eq(true)
end
end

View File

@ -0,0 +1,9 @@
require 'spec_helper'
describe Admin::EmbeddingController do
it "is a subclass of AdminController" do
expect(Admin::EmbeddingController < Admin::AdminController).to eq(true)
end
end

View File

@ -11,7 +11,6 @@ describe EmbedController do
end
it "raises an error with a missing host" do
SiteSetting.embeddable_hosts = nil
get :comments, embed_url: embed_url
expect(response).not_to be_success
end
@ -19,7 +18,7 @@ describe EmbedController do
context "by topic id" do
before do
SiteSetting.embeddable_hosts = host
Fabricate(:embeddable_host)
controller.request.stubs(:referer).returns('http://eviltrout.com/some-page')
end
@ -31,9 +30,7 @@ describe EmbedController do
end
context "with a host" do
before do
SiteSetting.embeddable_hosts = host
end
let!(:embeddable_host) { Fabricate(:embeddable_host) }
it "raises an error with no referer" do
get :comments, embed_url: embed_url
@ -68,7 +65,9 @@ describe EmbedController do
context "with multiple hosts" do
before do
SiteSetting.embeddable_hosts = "#{host}\nhttp://discourse.org\nhttps://example.com/1234"
Fabricate(:embeddable_host)
Fabricate(:embeddable_host, host: 'http://discourse.org')
Fabricate(:embeddable_host, host: 'https://example.com/1234')
end
context "success" do

View File

@ -1,27 +1,4 @@
Fabricator(:category) do
name { sequence(:name) { |n| "Amazing Category #{n}" } }
user
end
Fabricator(:diff_category, from: :category) do
name "Different Category"
user
end
Fabricator(:happy_category, from: :category) do
name 'Happy Category'
slug 'happy'
user
end
Fabricator(:private_category, from: :category) do
transient :group
name 'Private Category'
slug 'private'
user
after_build do |cat, transients|
cat.update!(read_restricted: true)
cat.category_groups.build(group_id: transients[:group].id, permission_type: CategoryGroup.permission_types[:full])
end
Fabricator(:embeddable_host) do
host "eviltrout.com"
category
end

View File

@ -0,0 +1,27 @@
Fabricator(:category) do
name { sequence(:name) { |n| "Amazing Category #{n}" } }
user
end
Fabricator(:diff_category, from: :category) do
name "Different Category"
user
end
Fabricator(:happy_category, from: :category) do
name 'Happy Category'
slug 'happy'
user
end
Fabricator(:private_category, from: :category) do
transient :group
name 'Private Category'
slug 'private'
user
after_build do |cat, transients|
cat.update!(read_restricted: true)
cat.category_groups.build(group_id: transients[:group].id, permission_type: CategoryGroup.permission_types[:full])
end
end

View File

@ -0,0 +1,40 @@
require 'spec_helper'
describe EmbeddableHost do
it "trims http" do
eh = EmbeddableHost.new(host: 'http://example.com')
expect(eh).to be_valid
expect(eh.host).to eq('example.com')
end
it "trims https" do
eh = EmbeddableHost.new(host: 'https://example.com')
expect(eh).to be_valid
expect(eh.host).to eq('example.com')
end
it "trims paths" do
eh = EmbeddableHost.new(host: 'https://example.com/1234/45')
expect(eh).to be_valid
expect(eh.host).to eq('example.com')
end
describe "allows_embeddable_host" do
let!(:host) { Fabricate(:embeddable_host) }
it 'works as expected' do
expect(EmbeddableHost.host_allowed?('http://eviltrout.com')).to eq(true)
expect(EmbeddableHost.host_allowed?('https://eviltrout.com')).to eq(true)
expect(EmbeddableHost.host_allowed?('https://not-eviltrout.com')).to eq(false)
end
it 'works with multiple hosts' do
Fabricate(:embeddable_host, host: 'discourse.org')
expect(EmbeddableHost.host_allowed?('http://eviltrout.com')).to eq(true)
expect(EmbeddableHost.host_allowed?('http://discourse.org')).to eq(true)
end
end
end

View File

@ -4,36 +4,6 @@ require_dependency 'site_setting_extension'
describe SiteSetting do
describe "allows_embeddable_host" do
it 'works as expected' do
SiteSetting.embeddable_hosts = 'eviltrout.com'
expect(SiteSetting.allows_embeddable_host?('http://eviltrout.com')).to eq(true)
expect(SiteSetting.allows_embeddable_host?('https://eviltrout.com')).to eq(true)
expect(SiteSetting.allows_embeddable_host?('https://not-eviltrout.com')).to eq(false)
end
it 'works with a http host' do
SiteSetting.embeddable_hosts = 'http://eviltrout.com'
expect(SiteSetting.allows_embeddable_host?('http://eviltrout.com')).to eq(true)
expect(SiteSetting.allows_embeddable_host?('https://eviltrout.com')).to eq(true)
expect(SiteSetting.allows_embeddable_host?('https://not-eviltrout.com')).to eq(false)
end
it 'works with a https host' do
SiteSetting.embeddable_hosts = 'https://eviltrout.com'
expect(SiteSetting.allows_embeddable_host?('http://eviltrout.com')).to eq(true)
expect(SiteSetting.allows_embeddable_host?('https://eviltrout.com')).to eq(true)
expect(SiteSetting.allows_embeddable_host?('https://not-eviltrout.com')).to eq(false)
end
it 'works with multiple hosts' do
SiteSetting.embeddable_hosts = "https://eviltrout.com\nhttps://discourse.org"
expect(SiteSetting.allows_embeddable_host?('http://eviltrout.com')).to eq(true)
expect(SiteSetting.allows_embeddable_host?('http://discourse.org')).to eq(true)
end
end
describe 'topic_title_length' do
it 'returns a range of min/max topic title length' do
expect(SiteSetting.topic_title_length).to eq(

View File

@ -12,6 +12,7 @@ describe TopicEmbed do
let(:title) { "How to turn a fish from good to evil in 30 seconds" }
let(:url) { 'http://eviltrout.com/123' }
let(:contents) { "hello world new post <a href='/hello'>hello</a> <img src='/images/wat.jpg'>" }
let!(:embeddable_host) { Fabricate(:embeddable_host) }
it "returns nil when the URL is malformed" do
expect(TopicEmbed.import(user, "invalid url", title, contents)).to eq(nil)
@ -33,6 +34,8 @@ describe TopicEmbed do
expect(post.topic.has_topic_embed?).to eq(true)
expect(TopicEmbed.where(topic_id: post.topic_id)).to be_present
expect(post.topic.category).to eq(embeddable_host.category)
end
it "Supports updating the post" do

View File

@ -1238,7 +1238,7 @@ describe Topic do
it "doesn't return topics from muted categories" do
user = Fabricate(:user)
category = Fabricate(:category)
topic = Fabricate(:topic, category: category)
Fabricate(:topic, category: category)
CategoryUser.set_notification_level_for_category(user, CategoryUser.notification_levels[:muted], category.id)
@ -1247,7 +1247,7 @@ describe Topic do
it "doesn't return topics from TL0 users" do
new_user = Fabricate(:user, trust_level: 0)
topic = Fabricate(:topic, user_id: new_user.id)
Fabricate(:topic, user_id: new_user.id)
expect(Topic.for_digest(user, 1.year.ago, top_order: true)).to be_blank
end
@ -1397,10 +1397,16 @@ describe Topic do
end
describe "expandable_first_post?" do
let(:topic) { Fabricate.build(:topic) }
it "is false if embeddable_host is blank" do
expect(topic.expandable_first_post?).to eq(false)
end
describe 'with an emeddable host' do
before do
SiteSetting.embeddable_hosts = "http://eviltrout.com"
Fabricate(:embeddable_host)
SiteSetting.embed_truncate = true
topic.stubs(:has_topic_embed?).returns(true)
end
@ -1408,12 +1414,6 @@ describe Topic do
it "is true with the correct settings and topic_embed" do
expect(topic.expandable_first_post?).to eq(true)
end
it "is false if embeddable_host is blank" do
SiteSetting.embeddable_hosts = nil
expect(topic.expandable_first_post?).to eq(false)
end
it "is false if embed_truncate? is false" do
SiteSetting.embed_truncate = false
expect(topic.expandable_first_post?).to eq(false)
@ -1425,6 +1425,8 @@ describe Topic do
end
end
end
it "has custom fields" do
topic = Fabricate(:topic)
expect(topic.custom_fields["a"]).to eq(nil)

View File

@ -39,13 +39,17 @@ const _moreWidgets = [
{id: 224, name: 'Good Repellant'}
];
const fruits = [{id: 1, name: 'apple', farmer_id: 1, category_id: 4},
{id: 2, name: 'banana', farmer_id: 1, category_id: 3},
{id: 3, name: 'grape', farmer_id: 2, category_id: 5}];
const fruits = [{id: 1, name: 'apple', farmer_id: 1, color_ids: [1,2], category_id: 4},
{id: 2, name: 'banana', farmer_id: 1, color_ids: [3], category_id: 3},
{id: 3, name: 'grape', farmer_id: 2, color_ids: [2], category_id: 5}];
const farmers = [{id: 1, name: 'Old MacDonald'},
{id: 2, name: 'Luke Skywalker'}];
const colors = [{id: 1, name: 'Red'},
{id: 2, name: 'Green'},
{id: 3, name: 'Yellow'}];
function loggedIn() {
return !!Discourse.User.current();
}
@ -221,12 +225,11 @@ export default function() {
this.get('/fruits/:id', function() {
const fruit = fruits[0];
return response({ __rest_serializer: "1", fruit, farmers: [farmers[0]] });
return response({ __rest_serializer: "1", fruit, farmers, colors });
});
this.get('/fruits', function() {
return response({ __rest_serializer: "1", fruits, farmers });
return response({ __rest_serializer: "1", fruits, farmers, colors });
});
this.get('/widgets/:widget_id', function(request) {

View File

@ -106,19 +106,31 @@ test('destroyRecord when new', function(assert) {
});
test('find embedded', function() {
test('find embedded', function(assert) {
const store = createStore();
return store.find('fruit', 1).then(function(f) {
ok(f.get('farmer'), 'it has the embedded object');
ok(f.get('category'), 'categories are found automatically');
return store.find('fruit', 2).then(function(f) {
assert.ok(f.get('farmer'), 'it has the embedded object');
const fruitCols = f.get('colors');
assert.equal(fruitCols.length, 2);
assert.equal(fruitCols[0].get('id'), 1);
assert.equal(fruitCols[1].get('id'), 2);
assert.ok(f.get('category'), 'categories are found automatically');
});
});
test('findAll embedded', function() {
test('findAll embedded', function(assert) {
const store = createStore();
return store.findAll('fruit').then(function(fruits) {
equal(fruits.objectAt(0).get('farmer.name'), 'Old MacDonald');
equal(fruits.objectAt(0).get('farmer'), fruits.objectAt(1).get('farmer'), 'points at the same object');
equal(fruits.objectAt(2).get('farmer.name'), 'Luke Skywalker');
assert.equal(fruits.objectAt(0).get('farmer.name'), 'Old MacDonald');
assert.equal(fruits.objectAt(0).get('farmer'), fruits.objectAt(1).get('farmer'), 'points at the same object');
const fruitCols = fruits.objectAt(0).get('colors');
assert.equal(fruitCols.length, 2);
assert.equal(fruitCols[0].get('id'), 1);
assert.equal(fruitCols[1].get('id'), 2);
assert.equal(fruits.objectAt(2).get('farmer.name'), 'Luke Skywalker');
});
});