diff --git a/app/api/v1/entities/content_item.rb b/app/api/v1/entities/content_item.rb index 5234f34f4..b3bb0bd93 100644 --- a/app/api/v1/entities/content_item.rb +++ b/app/api/v1/entities/content_item.rb @@ -3,11 +3,6 @@ module Entities class ContentItem < Grape::Entity expose :id, documentation: {type: "Integer", desc: "Content Item ID", required: true} expose :publish_state, documentation: {type: "String", desc: "Publish state", required: true} - expose :published_at, documentation: {type: "dateTime", desc: "Date published", required: true} - expose :expired_at, documentation: {type: "dateTime", desc: "Date to expire", required: true} - expose :author, documentation: {type: "dateTime", desc: "Date published", required: true} do |content_item| - content_item.author.fullname - end expose :creator, documentation: {type: "dateTime", desc: "Date published", required: true} do |content_item| content_item.creator.fullname end diff --git a/app/api/v1/resources/content_items.rb b/app/api/v1/resources/content_items.rb index 15b52fcb3..5c5f17f51 100644 --- a/app/api/v1/resources/content_items.rb +++ b/app/api/v1/resources/content_items.rb @@ -1,13 +1,19 @@ module V1 module Resources class ContentItems < Grape::API + helpers ::V1::Helpers::SharedParamsHelper + helpers ::V1::Helpers::ParamsHelper + resource :content_items do + include Grape::Kaminari + paginate per_page: 25 + desc "Create a content item", { entity: ::V1::Entities::ContentItem, params: ::V1::Entities::ContentItem.documentation, nickname: "createContentItem" } params do requires :content_type_id, type: Integer, desc: "content type of content item" end post do - require_scope! 'create:content_items' + require_scope! 'modify:content_items' authorize! :create, ::ContentItem @content_item = ::ContentItem.new(params.merge(author_id: current_user.id, creator_id: current_user.id)) @@ -27,6 +33,37 @@ class ContentItems < Grape::API present @content_items, with: ::V1::Entities::ContentItem, field_items: true end + + desc 'Show published content items', { entity: ::V1::Entities::ContentItem, nickname: "contentItemsFeed" } + params do + use :pagination + requires :content_type_name, type: String, desc: 'ContentType of ContentItem' + end + get :feed do + require_scope! 'view:content_items' + authorize! :view, ::ContentItem + + last_updated_at = ContentItem.last_updated_at + params_hash = Digest::MD5.hexdigest(declared(params).to_s) + cache_key = "feed-#{last_updated_at}-#{current_tenant.id}-#{params_hash}" + + content_items = ::Rails.cache.fetch(cache_key, expires_in: 30.minutes, race_condition_ttl: 10) do + content_items = ::GetContentItems.call(params: declared(clean_params(params), include_missing: false), tenant: current_tenant, published: true).content_items + paginated_content_items = paginate(content_items).records.to_a + {records: paginated_content_items, headers: header} + end + + header.merge!(content_items[:headers]) + ::V1::Entities::ContentItem.represent content_items[:records], field_items: true + end + + desc 'Show a published content item', { entity: ::V1::Entities::ContentItem, nickname: "showFeedContentItem" } + get 'feed/*id' do + @content_item = ::GetContentItem.call(id: params[:id], published: true, tenant: current_tenant.id).content_item + not_found! unless @content_item + authorize! :view, @content_item + present @content_item, with: ::V1::Entities::ContentItem, field_items: true + end end end end diff --git a/app/assets/javascripts/legacy/controllers/webpages/edit.js b/app/assets/javascripts/legacy/controllers/webpages/edit.js index 2a5575adf..367d907ae 100644 --- a/app/assets/javascripts/legacy/controllers/webpages/edit.js +++ b/app/assets/javascripts/legacy/controllers/webpages/edit.js @@ -72,4 +72,16 @@ angular.module('cortex.controllers.webpages.edit', [ page: page.page }); } + + $scope.appendEditingParamsToUrl = function(url) { + var urlHasParams = _.includes(url, '?'); + + if (urlHasParams) { + url = url + '&editing_mode=1&disable_redirects=1'; + } else { + url = url + '?editing_mode=1&disable_redirects=1'; + } + + return url; + } }); diff --git a/app/assets/legacy_templates/webpages/edit.html b/app/assets/legacy_templates/webpages/edit.html index 97d86b6d8..7922ace94 100644 --- a/app/assets/legacy_templates/webpages/edit.html +++ b/app/assets/legacy_templates/webpages/edit.html @@ -129,6 +129,6 @@ - + diff --git a/app/interactors/get_content_item.rb b/app/interactors/get_content_item.rb new file mode 100644 index 000000000..6ac89dc1c --- /dev/null +++ b/app/interactors/get_content_item.rb @@ -0,0 +1,12 @@ +class GetContentItem + include Interactor + + def call + content_item = ::ContentItem + #content_item = content_item.find_by_tenant_id(context.tenant) if context.tenant + #content_item = content_item.published if context.published + #content_item = content_item.find_by_id_or_slug(context.id) + content_item = content_item.find_by_id(context.id) + context.content_item = content_item + end +end diff --git a/app/interactors/get_content_items.rb b/app/interactors/get_content_items.rb new file mode 100644 index 000000000..886cdea32 --- /dev/null +++ b/app/interactors/get_content_items.rb @@ -0,0 +1,8 @@ +class GetContentItems + include Interactor + + def call + content_items = ContentType.find_by_name(context.params.content_type_name.titleize).content_items.order(created_at: :desc) + context.content_items = content_items + end +end diff --git a/app/models/content_item.rb b/app/models/content_item.rb index 3a2117360..4e2a88edb 100644 --- a/app/models/content_item.rb +++ b/app/models/content_item.rb @@ -3,18 +3,7 @@ class ContentItem < ApplicationRecord include Elasticsearch::Model include Elasticsearch::Model::Callbacks - state_machine do - state :draft - state :scheduled - - event :schedule do - transitions :to => :scheduled, :from => [:draft] - end - - event :draft do - transitions :to => :draft, :from => [:scheduled] - end - end + scope :last_updated_at, -> { order(updated_at: :desc).select('updated_at').first.updated_at } acts_as_paranoid @@ -32,6 +21,19 @@ class ContentItem < ApplicationRecord after_save :index after_save :update_tag_lists + state_machine do + state :draft + state :scheduled + + event :schedule do + transitions :to => :scheduled, :from => [:draft] + end + + event :draft do + transitions :to => :draft, :from => [:scheduled] + end + end + def self.taggable_fields Field.select { |field| field.field_type_instance.is_a?(TagFieldType) }.map { |field_item| field_item.name.parameterize('_') } end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index c9abd248b..d3d7aabd9 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -39,7 +39,8 @@ optional_scopes 'view:users', 'modify:users', 'view:tenants', 'modify:tenants', 'view:posts', 'modify:posts', 'view:media', 'modify:media', 'view:applications', 'modify:applications', 'view:bulk_jobs', 'modify:bulk_jobs', 'view:documents', 'modify:documents', - 'view:snippets', 'modify:snippets', 'view:webpages', 'modify:webpages', 'view:content_types' + 'view:snippets', 'modify:snippets', 'view:webpages', 'modify:webpages', 'view:content_types', + 'modify:content_types', 'view:content_items', 'modify:content_items' # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then diff --git a/db/schema.rb b/db/schema.rb index e0da3f489..a7efa7be7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -472,8 +472,8 @@ t.string "dynamic_yield_sku" t.string "dynamic_yield_category" t.jsonb "tables_widget" - t.jsonb "accordion_group_widget" t.jsonb "charts_widget" + t.jsonb "accordion_group_widget" t.index ["user_id"], name: "index_webpages_on_user_id", using: :btree end diff --git a/db/seeds.rb b/db/seeds.rb index e5d17cd1c..623bf11e5 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -12,9 +12,7 @@ job_phase: initial_post_seed.job_phase, display: initial_post_seed.display, copyright_owner: initial_post_seed.copyright_owner, - categories: [Category.first], - primary_category: Category.first, - author: User.first) + author: Author.first) existing_tenant = Tenant.find_by_name(tenant_seed.name) diff --git a/lib/tasks/employer/integration.rake b/lib/tasks/employer/integration.rake new file mode 100644 index 000000000..b2efaaa6c --- /dev/null +++ b/lib/tasks/employer/integration.rake @@ -0,0 +1,261 @@ +Bundler.require(:default, Rails.env) + +namespace :employer do + namespace :integration do + desc 'Seed Employer Integration ContentType and Fields' + task seed: :environment do + def category_tree + tree = Tree.new + tree.add_node({ name: "Category 1" }) + tree.add_node({ name: "Category 2" }) + tree.add_node({ name: "Category 3" }) + tree + end + + puts 'Creating Employer Integration ContentType...' + integration = ContentType.new({ + name: "Employer Integration", + description: "3rd Party CareerBuilder Integrations", + icon: "device hub", + creator_id: 1, + contract_id: 1, + publishable: true + }) + integration.save! + + puts "Creating Fields..." + integration.fields.new(name: 'Body', field_type: 'text_field_type', metadata: {parse_widgets: true}, validations: { presence: true }) + integration.fields.new(name: 'Title', field_type: 'text_field_type', validations: {presence: true}) + integration.fields.new(name: 'Description', field_type: 'text_field_type', validations: {presence: true}) + integration.fields.new(name: 'Slug', field_type: 'text_field_type', validations: {presence: true, uniqueness: true}) + integration.fields.new(name: 'Tags', field_type: 'tag_field_type') + integration.fields.new(name: 'Publish Date', field_type: 'date_time_field_type', metadata: {state: 'Published'}) + integration.fields.new(name: 'Expiration Date', field_type: 'date_time_field_type', metadata: {state: 'Expired'}) + integration.fields.new(name: 'SEO Title', field_type: 'text_field_type', validations: {presence: true, uniqueness: true }) + integration.fields.new(name: 'SEO Description', field_type: 'text_field_type', validations: {presence: true}) + integration.fields.new(name: 'SEO Keywords', field_type: 'tag_field_type') + integration.fields.new(name: 'No Index', field_type: 'boolean_field_type') + integration.fields.new(name: 'No Follow', field_type: 'boolean_field_type') + integration.fields.new(name: 'No Snippet', field_type: 'boolean_field_type') + integration.fields.new(name: 'No ODP', field_type: 'boolean_field_type') + integration.fields.new(name: 'No Archive', field_type: 'boolean_field_type') + integration.fields.new(name: 'No Image Index', field_type: 'boolean_field_type') + integration.fields.new(name: 'Categories', field_type: 'tree_field_type', metadata: {allowed_values: category_tree}) + integration.fields.new(name: 'Featured Image', field_type: 'content_item_field_type', + metadata: { + field_name: 'Asset' + }) + + puts "Saving Employer Integration..." + integration.save! + + puts "Creating Wizard Decorators..." + wizard_hash = { + "steps": [ + { + "name": "Write", + "columns": [ + { + "grid_width": 12, + "elements": [ + { + "id": integration.fields.find_by_name('Title').id + }, + { + "id": integration.fields.find_by_name('Body').id, + "render_method": "wysiwyg", + "input": { + "display": { + "styles": { + "height": "500px" + } + } + } + } + ] + } + ] + }, + + { + "name": "Details", + "columns": [ + { + "grid_width": 6, + "elements": [ + { + "id": integration.fields.find_by_name('Description').id, + "tooltip": 'This is a short description and will be used as the preview text for an employer before they click into the integration.' + }, + { + "id": integration.fields.find_by_name('Publish Date').id + }, + { + "id": integration.fields.find_by_name('Expiration Date').id + } + ] + }, + { + "grid_width": 6, + "elements": [ + { + "id": integration.fields.find_by_name('Tags').id + }, + { + "id": integration.fields.find_by_name('Slug').id, + "tooltip": "This is your integrations's URL. Between each word, place a hyphen. Best if between 35-50 characters and don't include years/dates." + } + ] + } + ] + }, + { + "name": "Categorize", + "columns": [ + { + "grid_width": 4, + "elements": [ + { + "id": integration.fields.find_by_name('Categories').id, + "render_method": "checkboxes" + } + ] + }, + { + "grid_width": 8, + "elements": [ + { + "id": integration.fields.find_by_name('Featured Image').id, + "render_method": "popup" + } + ] + } + ] + }, + { + "name": "Search", + "columns": [ + { + "grid_width": 6, + "elements": [ + { + "id": integration.fields.find_by_name('SEO Title').id, + "tooltip": 'Please use <70 characters for your SEO title for optimal appearance in search results.' + }, + { + "id": integration.fields.find_by_name('SEO Description').id, + "tooltip": 'The description should optimally be between 150-160 characters and keyword rich.' + }, + { + "id": integration.fields.find_by_name('SEO Keywords').id, + "tooltip": 'Utilize the recommended keywords as tags to boost your SEO performance.' + } + ] + }, + { + "grid_width": 6, + "description": "Select these if you don't want your integration to be indexed by search engines like Google", + "elements": [ + { + "id": integration.fields.find_by_name('No Index').id + }, + { + "id": integration.fields.find_by_name('No Follow').id + }, + { + "id": integration.fields.find_by_name('No Snippet').id + }, + { + "id": integration.fields.find_by_name('No ODP').id + }, + { + "id": integration.fields.find_by_name('No Archive').id + }, + { + "id": integration.fields.find_by_name('No Image Index').id + } + ] + } + ] + } + ] + } + + wizard_decorator = Decorator.new(name: "Wizard", data: wizard_hash) + wizard_decorator.save! + + ContentableDecorator.create!({ + decorator_id: wizard_decorator.id, + contentable_id: integration.id, + contentable_type: 'ContentType' + }) + + puts "Creating Index Decorators..." + index_hash = { + "columns": + [ + { + "name": "Title", + "grid_width": 3, + "cells": [{ + "field": { + "id": integration.fields.find_by_name('Title').id + } + }] + }, + { + "name": "Integration Details", + "cells": [ + { + "field": { + "id": integration.fields.find_by_name('Description').id + }, + "display": { + "classes": [ + "bold", + "upcase" + ] + } + }, + { + "field": { + "id": integration.fields.find_by_name('Slug').id + } + }, + { + "field": { + "method": "publish_state" + } + } + ] + }, + { + "name": "Tags", + "cells": [ + { + "field": { + "id": integration.fields.find_by_name('Tags').id + }, + "display": { + "classes": [ + "tag", + "rounded" + ] + } + } + ] + } + ] + } + + index_decorator = Decorator.new(name: "Index", data: index_hash) + index_decorator.save! + + ContentableDecorator.create!({ + decorator_id: index_decorator.id, + contentable_id: integration.id, + contentable_type: 'ContentType' + }) + end + end +end