mirror of
https://github.com/indentlabs/notebook.git
synced 2025-10-26 11:19:22 +00:00
Merge pull request #1003 from indentlabs/august-perf
Site-wide performance improvements
This commit is contained in:
commit
e34a211813
1
Gemfile
1
Gemfile
@ -42,7 +42,6 @@ gem 'paranoia'
|
||||
# Javascript
|
||||
gem 'coffee-rails'
|
||||
gem 'rails-jquery-autocomplete'
|
||||
gem 'animate-rails'
|
||||
gem 'webpacker'
|
||||
gem 'react-rails'
|
||||
|
||||
|
||||
@ -108,8 +108,6 @@ GEM
|
||||
activerecord (>= 4.2)
|
||||
addressable (2.7.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
animate-rails (1.0.10)
|
||||
rails
|
||||
authority (3.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
autoprefixer-rails (9.8.4)
|
||||
@ -1629,7 +1627,6 @@ PLATFORMS
|
||||
DEPENDENCIES
|
||||
active_storage_validations
|
||||
acts_as_list
|
||||
animate-rails
|
||||
authority
|
||||
aws-sdk (~> 3.1)
|
||||
aws-sdk-s3
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
*= require font-awesome
|
||||
*= require medium-editor/medium-editor
|
||||
*= require medium-editor/themes/beagle
|
||||
*= require animate
|
||||
*= require tribute
|
||||
*= require_tree .
|
||||
*/
|
||||
|
||||
48
app/authorizers/content_page_authorizer.rb
Normal file
48
app/authorizers/content_page_authorizer.rb
Normal file
@ -0,0 +1,48 @@
|
||||
class ContentPageAuthorizer < CoreContentAuthorizer
|
||||
def self.creatable_by?(user)
|
||||
return false unless user.present?
|
||||
return false if ENV.key?('CONTENT_BLACKLIST') && ENV['CONTENT_BLACKLIST'].split(',').include?(user.email)
|
||||
|
||||
if resource.page_type == 'Universe'
|
||||
return true if PermissionService.user_has_fewer_owned_universes_than_plan_limit?(user: user)
|
||||
|
||||
else
|
||||
is_premium_page = Rails.application.config.content_types[:premium].include?(resource.page_type)
|
||||
return true if !is_premium_page
|
||||
return true if is_premium_page && PermissionService.user_is_on_premium_plan?(user: user)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
def readable_by?(user)
|
||||
return true if PermissionService.content_is_public?(content: resource)
|
||||
return true if PermissionService.user_owns_content?(user: user, content: resource)
|
||||
|
||||
if resource.page_type == 'Universe'
|
||||
return true if PermissionService.user_can_contribute_to_universe?(user: user, universe: resource)
|
||||
else
|
||||
return true if PermissionService.user_can_contribute_to_containing_universe?(user: user, content: resource)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
def updatable_by?(user)
|
||||
return true if PermissionService.user_owns_content?(user: user, content: resource)
|
||||
|
||||
if resource.page_type == 'Universe'
|
||||
return true if PermissionService.user_can_contribute_to_universe?(user: user, universe: resource)
|
||||
else
|
||||
return true if PermissionService.user_can_contribute_to_containing_universe?(user: user, content: resource)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
def deletable_by?(user)
|
||||
[
|
||||
PermissionService.user_owns_content?(user: user, content: resource)
|
||||
].any?
|
||||
end
|
||||
end
|
||||
@ -1,3 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
class ApplicationController < ActionController::Base
|
||||
protect_from_forgery
|
||||
|
||||
@ -5,7 +6,6 @@ class ApplicationController < ActionController::Base
|
||||
before_action :set_universe_scope
|
||||
|
||||
before_action :cache_most_used_page_information
|
||||
before_action :cache_forums_unread_counts
|
||||
|
||||
before_action :set_metadata
|
||||
|
||||
@ -27,7 +27,7 @@ class ApplicationController < ActionController::Base
|
||||
session.delete(:universe_id)
|
||||
elsif params[:universe].is_a?(String) && params[:universe].to_i.to_s == params[:universe]
|
||||
found_universe = Universe.find_by(id: params[:universe])
|
||||
found_universe = nil unless current_user.universes.include?(found_universe) || current_user.contributable_universes.include?(found_universe)
|
||||
found_universe = nil unless found_universe.user_id == current_user.id || found_universe.contributors.pluck(:user_id).include?(current_user.id)
|
||||
session[:universe_id] = found_universe.id if found_universe
|
||||
end
|
||||
end
|
||||
@ -36,52 +36,116 @@ class ApplicationController < ActionController::Base
|
||||
def set_universe_scope
|
||||
if user_signed_in? && session.key?(:universe_id)
|
||||
@universe_scope = Universe.find_by(id: session[:universe_id])
|
||||
@universe_scope = nil unless current_user.universes.include?(@universe_scope) || current_user.contributable_universes.include?(@universe_scope)
|
||||
|
||||
if @universe_scope && @universe_scope.user_id != current_user.try(:id)
|
||||
# Verify the current user has access to this universe by looking up their
|
||||
# universe contributorship
|
||||
contributorship = Contributor.find_by(
|
||||
user: current_user,
|
||||
universe: @universe_scope
|
||||
)
|
||||
|
||||
if contributorship.nil?
|
||||
# If the user doesn't have current contributor access to this universe,
|
||||
# then revert back to unscoped universe actions
|
||||
@universe_scope = nil
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
@universe_scope = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Cache some super-common stuff we need for every page. For example, content lists for the side nav.
|
||||
# Cache some super-common stuff we need for every page. For example, content lists for the side nav. This is a catch-all for most pages that render
|
||||
# UI, but methods are also free to skip this filter and call the individual cache methods they need instead.
|
||||
def cache_most_used_page_information
|
||||
return unless user_signed_in?
|
||||
|
||||
cache_activated_content_types
|
||||
cache_current_user_content
|
||||
cache_notifications
|
||||
cache_recently_edited_pages
|
||||
cache_forums_unread_counts
|
||||
end
|
||||
|
||||
def cache_activated_content_types
|
||||
@activated_content_types ||= if user_signed_in?
|
||||
(
|
||||
# Use config to dictate order, but AND to only include what a user has turned on
|
||||
Rails.application.config.content_type_names[:all] & current_user.user_content_type_activators.pluck(:content_type)
|
||||
)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def cache_current_user_content
|
||||
return if @current_user_content
|
||||
|
||||
@current_user_content = {}
|
||||
return unless user_signed_in?
|
||||
|
||||
@activated_content_types = (
|
||||
Rails.application.config.content_types[:all].map(&:name) & # Use config to dictate order, but AND to only include what a user has turned on
|
||||
current_user.user_content_type_activators.pluck(:content_type)
|
||||
)
|
||||
cache_activated_content_types
|
||||
|
||||
# We always want to cache Universes, even if they aren't explicitly turned on.
|
||||
@current_user_content = current_user.content(content_types: @activated_content_types + ['Universe'], universe_id: @universe_scope.try(:id))
|
||||
@current_user_content = current_user.content(
|
||||
content_types: @activated_content_types + [Universe.name],
|
||||
universe_id: @universe_scope.try(:id)
|
||||
)
|
||||
|
||||
# Likewise, we should also always cache Timelines & Documents
|
||||
if @universe_scope
|
||||
@current_user_content['Timeline'] = current_user.timelines.where(universe_id: @universe_scope.try(:id)).to_a
|
||||
@current_user_content['Document'] = current_user.linkable_documents.includes([:user]).where(universe_id: @universe_scope.try(:id)).order('updated_at DESC').to_a
|
||||
@current_user_content['Document'] = current_user.documents.where(universe_id: @universe_scope.try(:id)).order('updated_at DESC').to_a
|
||||
else
|
||||
@current_user_content['Timeline'] = current_user.timelines.to_a
|
||||
@current_user_content['Document'] = current_user.linkable_documents.includes([:user]).order('updated_at DESC').to_a
|
||||
@current_user_content['Document'] = current_user.documents.order('updated_at DESC').to_a
|
||||
end
|
||||
end
|
||||
|
||||
# Fetch notifications
|
||||
@user_notifications = current_user.notifications.order('happened_at DESC').limit(100)
|
||||
def cache_notifications
|
||||
@user_notifications ||= if user_signed_in?
|
||||
current_user.notifications.order('happened_at DESC').limit(100)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Cache recently-edited pages
|
||||
@recently_edited_pages = @current_user_content.values.flatten
|
||||
.sort_by(&:updated_at)
|
||||
.last(50)
|
||||
.reverse
|
||||
def cache_recently_created_pages(amount=50)
|
||||
cache_current_user_content
|
||||
|
||||
@recently_created_pages = if user_signed_in?
|
||||
@current_user_content.values.flatten
|
||||
.sort_by(&:created_at)
|
||||
.last(amount)
|
||||
.reverse
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def cache_recently_edited_pages(amount=50)
|
||||
cache_current_user_content
|
||||
|
||||
@recently_edited_pages ||= if user_signed_in?
|
||||
@current_user_content.values.flatten
|
||||
.sort_by(&:updated_at)
|
||||
.last(amount)
|
||||
.reverse
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def cache_forums_unread_counts
|
||||
@unread_threads = if user_signed_in?
|
||||
@unread_threads ||= if user_signed_in?
|
||||
Thredded::Topic.unread_followed_by(current_user).count
|
||||
else
|
||||
0
|
||||
end
|
||||
|
||||
@unread_private_messages = if user_signed_in?
|
||||
@unread_private_messages ||= if user_signed_in?
|
||||
Thredded::PrivateTopic
|
||||
.for_user(current_user)
|
||||
.unread(current_user)
|
||||
@ -91,33 +155,60 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
end
|
||||
|
||||
def cache_contributable_universe_ids
|
||||
cache_current_user_content
|
||||
|
||||
@contributable_universe_ids ||= if user_signed_in?
|
||||
current_user.contributable_universe_ids + @current_user_content.fetch('Universe', []).map(&:id)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def cache_linkable_content_for_each_content_type
|
||||
linkable_classes = Rails.application.config.content_types[:all].map(&:name) & current_user.user_content_type_activators.pluck(:content_type)
|
||||
cache_contributable_universe_ids
|
||||
cache_current_user_content
|
||||
|
||||
linkable_classes = @activated_content_types
|
||||
linkable_classes += %w(Document Timeline)
|
||||
|
||||
@linkables_cache = {}
|
||||
@linkables_raw = {}
|
||||
linkable_classes.each do |class_name|
|
||||
# class_name = "Character"
|
||||
@linkables_cache = {} # Cache is list of [[page_name, page_id], [page_name, page_id], ...]
|
||||
@linkables_raw = {} # Raw is list of objects [#{page}, #{page}, ...]
|
||||
|
||||
@linkables_cache[class_name] = current_user
|
||||
.send("linkable_#{class_name.downcase.pluralize}")
|
||||
.in_universe(@universe_scope)
|
||||
@current_user_content.each do |page_type, content_list|
|
||||
# We already have our own list of content by the current user in @current_user_content,
|
||||
# so all we need to grab is additional pages in contributable universes
|
||||
@linkables_raw[page_type] = @current_user_content[page_type]
|
||||
|
||||
if @content.present? && @content.persisted?
|
||||
@linkables_cache[class_name] = @linkables_cache[class_name]
|
||||
.in_universe(@content.universe)
|
||||
.reject { |content| content.class.name == class_name && content.id == @content.id }
|
||||
if !@universe_scope && @contributable_universe_ids.any?
|
||||
existing_page_ids = @linkables_raw[page_type].map(&:id)
|
||||
|
||||
pages_to_add = if page_type == Universe.name
|
||||
page_type.constantize.where(id: @contributable_universe_ids)
|
||||
.where.not(id: existing_page_ids)
|
||||
else
|
||||
page_type.constantize.where(universe_id: @contributable_universe_ids)
|
||||
.where.not(id: existing_page_ids)
|
||||
end
|
||||
|
||||
filtered_fields = ContentPage.polymorphic_content_fields.map(&:to_s)
|
||||
filtered_fields.push 'universe_id' unless page_type == Universe.name
|
||||
pages_to_add.each do |page_data|
|
||||
filtered_page_data = page_data.attributes.slice(*filtered_fields)
|
||||
@linkables_raw[page_type].push ContentPage.new(filtered_page_data)
|
||||
end
|
||||
end
|
||||
|
||||
@linkables_raw[class_name] = @linkables_cache[class_name]
|
||||
.sort_by { |p| p.name.downcase }
|
||||
.compact
|
||||
# We can't properly display or @-mention content without a name set, so we explicitly
|
||||
# reject it here. However, this is a bit of a code-smell: why is there content without
|
||||
# a name set?
|
||||
@linkables_raw[page_type].reject! { |page| page.name.nil? }
|
||||
|
||||
@linkables_cache[class_name] = @linkables_cache[class_name]
|
||||
.sort_by { |p| p.name.downcase }
|
||||
.map { |c| [c.name, c.id] }
|
||||
.compact
|
||||
# Finally, we want to sort our linkables cache once so we don't have to sort it again
|
||||
@linkables_raw[page_type].sort_by! { |page| page.name.downcase }.compact!
|
||||
|
||||
# Lastly, build our name/id cache as well
|
||||
@linkables_cache[page_type] = @linkables_raw[page_type].map { |page| [page.name, page.id] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
class ContentController < ApplicationController
|
||||
# todo we should probably spin off an Api::ContentController for #api_sort and anything else api-wise we need
|
||||
# frozen_string_literal: true
|
||||
|
||||
# TODO: we should probably spin off an Api::ContentController for #api_sort and anything else
|
||||
# api-wise we need
|
||||
class ContentController < ApplicationController
|
||||
before_action :authenticate_user!, except: [:show, :changelog, :api_sort] \
|
||||
+ Rails.application.config.content_types[:all_non_universe].map { |type| type.name.downcase.pluralize.to_sym }
|
||||
|
||||
skip_before_action :cache_most_used_page_information, only: [
|
||||
:name_field_update, :text_field_update, :tags_field_update, :universe_field_update, :api_sort
|
||||
]
|
||||
|
||||
before_action :migrate_old_style_field_values, only: [:show, :edit]
|
||||
|
||||
before_action :cache_linkable_content_for_each_content_type, only: [:new, :edit, :index]
|
||||
@ -21,27 +27,13 @@ class ContentController < ApplicationController
|
||||
@page_title = "My #{pluralized_content_name}"
|
||||
|
||||
# Create the default fields for this user if they don't have any already
|
||||
# TODO: uh, this probably doesn't belong here!
|
||||
@content_type_class.attribute_categories(current_user)
|
||||
|
||||
if @universe_scope.present? && @content_type_class != Universe
|
||||
@content = @universe_scope.send(pluralized_content_name)
|
||||
.includes(:page_tags, :image_uploads)
|
||||
.unarchived
|
||||
# Linkables cache is already scoped per-universe, includes contributor pages
|
||||
@content = @linkables_raw.fetch(@content_type_class.name, [])
|
||||
|
||||
@show_scope_notice = true
|
||||
else
|
||||
@content = (
|
||||
current_user.send(pluralized_content_name).unarchived.includes(:page_tags, :image_uploads) +
|
||||
current_user.send("contributable_#{pluralized_content_name}").unarchived.includes(:page_tags, :image_uploads)
|
||||
)
|
||||
|
||||
if @content_type_class != Universe
|
||||
my_universe_ids = current_user.universes.pluck(:id)
|
||||
@content.concat(@content_type_class.where(universe_id: my_universe_ids).unarchived)
|
||||
end
|
||||
end
|
||||
|
||||
@content = @content.to_a.flatten.uniq
|
||||
@show_scope_notice = @universe_scope.present? && @content_type_class != Universe
|
||||
|
||||
# Filters
|
||||
@page_tags = PageTag.where(
|
||||
@ -52,18 +44,18 @@ class ContentController < ApplicationController
|
||||
@filtered_page_tags = @page_tags.where(slug: params[:tag])
|
||||
@content.select! { |content| @filtered_page_tags.pluck(:page_id).include?(content.id) }
|
||||
end
|
||||
@page_tags = @page_tags.uniq(&:tag)
|
||||
|
||||
if params.key?(:favorite_only)
|
||||
@content.select!(&:favorite?)
|
||||
end
|
||||
|
||||
@page_tags = @page_tags.uniq(&:tag)
|
||||
|
||||
@content = @content.sort_by {|x| [x.favorite? ? 0 : 1, x.name] }
|
||||
|
||||
@questioned_content = @content.sample
|
||||
@attribute_field_to_question = SerendipitousService.question_for(@questioned_content)
|
||||
|
||||
# Uh, do we ever actually make JSON requests to logged-in user pages?
|
||||
respond_to do |format|
|
||||
format.html { render 'content/index' }
|
||||
format.json { render json: @content }
|
||||
@ -72,7 +64,7 @@ class ContentController < ApplicationController
|
||||
|
||||
def show
|
||||
content_type = content_type_from_controller(self.class)
|
||||
return redirect_to(root_path, notice: "That page doesn't exist!") unless valid_content_types.map(&:name).include?(content_type.name)
|
||||
return redirect_to(root_path, notice: "That page doesn't exist!") unless valid_content_types.include?(content_type.name)
|
||||
|
||||
@content = content_type.find_by(id: params[:id])
|
||||
return redirect_to(root_path, notice: "You don't have permission to view that content.") if @content.nil?
|
||||
@ -289,7 +281,7 @@ class ContentController < ApplicationController
|
||||
|
||||
def changelog
|
||||
content_type = content_type_from_controller(self.class)
|
||||
return redirect_to root_path unless valid_content_types.map(&:name).include?(content_type.name)
|
||||
return redirect_to root_path unless valid_content_types.include?(content_type.name)
|
||||
@content = content_type.find_by(id: params[:id])
|
||||
return redirect_to(root_path, notice: "You don't have permission to view that content.") if @content.nil?
|
||||
@serialized_content = ContentSerializer.new(@content)
|
||||
@ -421,6 +413,7 @@ class ContentController < ApplicationController
|
||||
set_entity
|
||||
referencing_page = @entity
|
||||
|
||||
# TODO: move this into a link mention update job
|
||||
valid_reference_ids = []
|
||||
referenced_page_codes = JSON.parse(attribute_value.value)
|
||||
referenced_page_codes.each do |page_code|
|
||||
@ -455,7 +448,7 @@ class ContentController < ApplicationController
|
||||
|
||||
# We also need to update the cached `name` field on the content page itself
|
||||
entity_type = entity_params.fetch(:entity_type)
|
||||
raise "Invalid entity type: #{entity_params.fetch(:entity_type)}" unless valid_content_types.map(&:name).include?(entity_params.fetch('entity_type'))
|
||||
raise "Invalid entity type: #{entity_params.fetch(:entity_type)}" unless valid_content_types.include?(entity_params.fetch('entity_type'))
|
||||
entity = entity_type.constantize.find_by(id: entity_params.fetch(:entity_id).to_i)
|
||||
entity.update(name: field_params.fetch('value', ''))
|
||||
end
|
||||
@ -469,35 +462,16 @@ class ContentController < ApplicationController
|
||||
attribute_value.value = text
|
||||
attribute_value.save!
|
||||
|
||||
# Create PageReferences for mentioned pages
|
||||
tokens = ContentFormatterService.tokens_to_replace(text)
|
||||
if tokens.any?
|
||||
set_entity
|
||||
UpdateTextAttributeReferencesJob.perform_later(attribute_value.id)
|
||||
|
||||
valid_reference_ids = []
|
||||
tokens.each do |token|
|
||||
reference = @entity.outgoing_page_references.find_or_initialize_by(
|
||||
referenced_page_type: token[:content_type],
|
||||
referenced_page_id: token[:content_id],
|
||||
attribute_field_id: @attribute_field.id,
|
||||
reference_type: 'mentioned'
|
||||
)
|
||||
reference.cached_relation_title = @attribute_field.label
|
||||
reference.save!
|
||||
|
||||
valid_reference_ids << reference.reload.id
|
||||
end
|
||||
|
||||
# Delete all other references still attached to this field, but not present in this request
|
||||
@entity.outgoing_page_references
|
||||
.where(attribute_field_id: @attribute_field.id)
|
||||
.where.not(id: valid_reference_ids)
|
||||
.destroy_all
|
||||
respond_to do |format|
|
||||
format.html { redirect_back(fallback_location: root_path, notice: "#{@attribute_field.label} updated!") }
|
||||
format.json { render json: attribute_value, status: :success }
|
||||
end
|
||||
end
|
||||
|
||||
def tags_field_update
|
||||
return unless valid_content_types.map(&:name).include?(entity_params.fetch('entity_type'))
|
||||
return unless valid_content_types.include?(entity_params.fetch('entity_type'))
|
||||
|
||||
@attribute_field = AttributeField.find_by(id: params[:field_id].to_i)
|
||||
attribute_value = @attribute_field.attribute_values.order('created_at desc').find_or_initialize_by(entity_params)
|
||||
@ -512,7 +486,7 @@ class ContentController < ApplicationController
|
||||
end
|
||||
|
||||
def universe_field_update
|
||||
return unless valid_content_types.map(&:name).include?(entity_params.fetch('entity_type'))
|
||||
return unless valid_content_types.include?(entity_params.fetch('entity_type'))
|
||||
|
||||
@attribute_field = AttributeField.find_by(id: params[:field_id].to_i)
|
||||
attribute_value = @attribute_field.attribute_values.order('created_at desc').find_or_initialize_by(entity_params)
|
||||
@ -580,7 +554,7 @@ class ContentController < ApplicationController
|
||||
end
|
||||
|
||||
def valid_content_types
|
||||
Rails.application.config.content_types[:all]
|
||||
Rails.application.config.content_type_names[:all]
|
||||
end
|
||||
|
||||
def initialize_object
|
||||
@ -656,7 +630,7 @@ class ContentController < ApplicationController
|
||||
def set_attributes_content_type
|
||||
@content_type = params[:content_type]
|
||||
# todo make this a before_action load_content_type
|
||||
unless valid_content_types.map { |c| c.name.downcase }.include?(@content_type)
|
||||
unless valid_content_types.map(&:downcase).include?(@content_type)
|
||||
raise "Invalid content type on attributes customization page: #{@content_type}"
|
||||
end
|
||||
@content_type_class = @content_type.titleize.constantize
|
||||
@ -671,7 +645,7 @@ class ContentController < ApplicationController
|
||||
entity_page_type = entity_params.fetch(:entity_type)
|
||||
entity_page_id = entity_params.fetch(:entity_id)
|
||||
|
||||
return unless valid_content_types.map(&:name).include?(entity_page_type)
|
||||
return unless valid_content_types.include?(entity_page_type)
|
||||
@entity = entity_page_type.constantize.find_by(id: entity_page_id)
|
||||
end
|
||||
|
||||
|
||||
@ -4,6 +4,8 @@ class DocumentsController < ApplicationController
|
||||
# todo Uh, this is a hack. The CSRF token on document editor model to add entities is being rejected... for whatever reason.
|
||||
skip_before_action :verify_authenticity_token, only: [:link_entity]
|
||||
|
||||
skip_before_action :cache_most_used_page_information, only: [:update]
|
||||
|
||||
before_action :set_document, only: [:show, :analysis, :plaintext, :queue_analysis, :edit, :destroy]
|
||||
before_action :set_sidenav_expansion, except: [:plaintext]
|
||||
before_action :set_navbar_color, except: [:plaintext]
|
||||
@ -181,21 +183,21 @@ class DocumentsController < ApplicationController
|
||||
|
||||
def update
|
||||
document = Document.with_deleted.find_or_initialize_by(id: params[:id])
|
||||
d_params = document_params.clone # TODO: why are we duplicating the params here?
|
||||
|
||||
unless document.updatable_by?(current_user)
|
||||
redirect_to(dashboard_path, notice: "You don't have permission to do that!")
|
||||
return
|
||||
end
|
||||
|
||||
# Only queue document mentions for analysis if the document body has changed
|
||||
DocumentMentionJob.perform_later(document.id) if d_params.key?(:body)
|
||||
|
||||
# We can't pass actual-nil from HTML (for no universe), so we pass a string instead and convert it back here.
|
||||
d_params = document_params.clone
|
||||
if d_params.fetch(:universe_id, nil) == "nil"
|
||||
d_params[:universe_id] = nil
|
||||
end
|
||||
|
||||
# Only queue document mentions for analysis if the document body has changed
|
||||
DocumentMentionJob.perform_later(document.id) if d_params.key?(:body)
|
||||
|
||||
update_page_tags(document) if document_tag_params
|
||||
|
||||
if document.update(d_params)
|
||||
|
||||
@ -25,8 +25,7 @@ class MainController < ApplicationController
|
||||
def dashboard
|
||||
@page_title = "My notebook"
|
||||
|
||||
set_random_content # for questions
|
||||
@attribute_field_to_question = SerendipitousService.question_for(@content)
|
||||
set_questionable_content # for questions
|
||||
end
|
||||
|
||||
def infostack
|
||||
@ -65,8 +64,7 @@ class MainController < ApplicationController
|
||||
@navbar_color = '#FF9800'
|
||||
@page_title = "Writing prompts"
|
||||
|
||||
set_random_content # for question
|
||||
@attribute_field_to_question = SerendipitousService.question_for(@content)
|
||||
set_questionable_content # for question
|
||||
end
|
||||
|
||||
# deprecated path just kept around for bookmarks for a while
|
||||
@ -75,13 +73,6 @@ class MainController < ApplicationController
|
||||
end
|
||||
|
||||
def recent_content
|
||||
# todo optimize this / use Attributes
|
||||
return [] if @activated_content_types.nil?
|
||||
|
||||
@recently_created_pages = @current_user_content.values.flatten
|
||||
.sort_by(&:created_at)
|
||||
.last(50)
|
||||
.reverse
|
||||
end
|
||||
|
||||
def for_writers
|
||||
@ -105,27 +96,8 @@ class MainController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def set_random_content
|
||||
@activated_content_types.shuffle.each do |content_type|
|
||||
if content_type == Universe.name
|
||||
if @universe_scope.present?
|
||||
@content = content_type.constantize.where(user: current_user, id: @universe_scope.id).includes(:user)
|
||||
else
|
||||
@content = content_type.constantize.where(user: current_user).includes(:user)
|
||||
end
|
||||
else
|
||||
if @universe_scope.present?
|
||||
# when we want to enable prompts for contributing universes we can remove the user:
|
||||
# selector here, but we will need to verify the user has permission to see the universe
|
||||
# when we do that, or else prompts could open leak
|
||||
@content = content_type.constantize.where(user: current_user, universe: @universe_scope).includes(:user, :universe)
|
||||
else
|
||||
@content = content_type.constantize.where(user: current_user).includes(:user, :universe)
|
||||
end
|
||||
end
|
||||
|
||||
@content = @content.sample
|
||||
return if @content.present?
|
||||
end
|
||||
def set_questionable_content
|
||||
@content = @current_user_content.except(*%w(Timeline Document)).values.flatten.sample
|
||||
@attribute_field_to_question = SerendipitousService.question_for(@content)
|
||||
end
|
||||
end
|
||||
|
||||
@ -7,6 +7,8 @@ class TimelinesController < ApplicationController
|
||||
|
||||
# GET /timelines
|
||||
def index
|
||||
cache_linkable_content_for_each_content_type
|
||||
|
||||
@timelines = current_user.timelines
|
||||
@page_title = "My timelines"
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ class DocumentEntityAnalysisJob < ApplicationJob
|
||||
def perform(*args)
|
||||
document_entity_id = args.shift
|
||||
|
||||
entity = DocumentEntity.find(document_entity_id)
|
||||
entity = DocumentEntity.find_by(id: document_entity_id)
|
||||
return unless entity.present?
|
||||
|
||||
Documents::Analysis::ThirdParty::IbmWatsonService.analyze_entity(document_entity_id)
|
||||
|
||||
36
app/jobs/update_text_attribute_references_job.rb
Normal file
36
app/jobs/update_text_attribute_references_job.rb
Normal file
@ -0,0 +1,36 @@
|
||||
class UpdateTextAttributeReferencesJob < ApplicationJob
|
||||
queue_as :mentions
|
||||
|
||||
def perform(*args)
|
||||
attribute_id = args.shift
|
||||
|
||||
attribute = Attribute.find_by(id: attribute_id)
|
||||
return unless attribute.present?
|
||||
|
||||
# Create PageReferences for mentioned pages
|
||||
tokens = ContentFormatterService.tokens_to_replace(attribute.value)
|
||||
if tokens.any?
|
||||
entity = attribute.entity
|
||||
|
||||
valid_reference_ids = []
|
||||
tokens.each do |token|
|
||||
reference = entity.outgoing_page_references.find_or_initialize_by(
|
||||
referenced_page_type: token[:content_type],
|
||||
referenced_page_id: token[:content_id],
|
||||
attribute_field_id: attribute.attribute_field_id,
|
||||
reference_type: 'mentioned'
|
||||
)
|
||||
reference.cached_relation_title = AttributeField.find_by(id: attribute.attribute_field_id).try(:label)
|
||||
reference.save!
|
||||
|
||||
valid_reference_ids << reference.reload.id
|
||||
end
|
||||
|
||||
# Delete all other references still attached to this field, but not present in this request
|
||||
entity.outgoing_page_references
|
||||
.where(attribute_field_id: attribute.attribute_field_id)
|
||||
.where.not(id: valid_reference_ids)
|
||||
.destroy_all
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -42,17 +42,28 @@ module HasAttributes
|
||||
end
|
||||
|
||||
def self.attribute_categories(user, show_hidden: false)
|
||||
# TODO: this is a code smell; we should probably either be whitelisting or fixing whatever is calling
|
||||
# this with the wrong models
|
||||
return [] if ['attribute_category', 'attribute_field'].include?(content_name)
|
||||
|
||||
# Cache the result in case we call this function multiple times this request
|
||||
@cached_attribute_categories_for_this_content = begin
|
||||
# Always include the flatfile categories (but create AR versions if they don't exist)
|
||||
categories_list = AttributeCategory.with_deleted.where(user: user).to_a
|
||||
full_fields_list = AttributeField.with_deleted.where(user: user, attribute_category_id: categories_list.map(&:id))
|
||||
categories = YAML.load_file(Rails.root.join('config', 'attributes', "#{content_name}.yml")).map do |category_name, details|
|
||||
category = ::AttributeCategory.with_deleted.find_or_initialize_by(
|
||||
entity_type: self.content_name,
|
||||
name: category_name.to_s,
|
||||
user: user
|
||||
)
|
||||
category = categories_list.detect do |persisted_category|
|
||||
persisted_category.entity_type == self.content_name &&
|
||||
persisted_category.name == category_name.to_s
|
||||
end
|
||||
if category.nil?
|
||||
category = AttributeCategory.new(
|
||||
entity_type: self.content_name,
|
||||
name: category_name.to_s,
|
||||
user: user
|
||||
)
|
||||
end
|
||||
|
||||
# Default new categories to some sane defaults
|
||||
unless category.persisted?
|
||||
category.icon = details[:icon]
|
||||
@ -60,13 +71,21 @@ module HasAttributes
|
||||
end
|
||||
|
||||
category.save! if user && category.new_record?
|
||||
fields_list = full_fields_list.select do |field|
|
||||
field.attribute_category_id == category.id
|
||||
end
|
||||
category.attribute_fields << details[:attributes].map do |field|
|
||||
af_field = category.attribute_fields.with_deleted.find_or_initialize_by(
|
||||
# label: field[:label],
|
||||
old_column_source: field[:name],
|
||||
user: user,
|
||||
field_type: field[:field_type].presence || "text_area"
|
||||
)
|
||||
af_field = fields_list.detect do |persisted_field|
|
||||
persisted_field.old_column_source == field[:name] &&
|
||||
persisted_field.field_type == field[:field_type].presence || "text_area"
|
||||
end
|
||||
if af_field.nil?
|
||||
af_field = category.attribute_fields.new(
|
||||
old_column_source: field[:name],
|
||||
user: user,
|
||||
field_type: field[:field_type].presence || "text_area"
|
||||
)
|
||||
end
|
||||
if af_field.label.nil?
|
||||
af_field.label = field[:label]
|
||||
end
|
||||
@ -180,6 +199,7 @@ module HasAttributes
|
||||
end
|
||||
end
|
||||
|
||||
# All of these helpers are spooky and rife for N+1s
|
||||
def name_field
|
||||
category_ids = AttributeCategory.where(
|
||||
user_id: user_id,
|
||||
|
||||
@ -16,18 +16,49 @@ module HasContent
|
||||
has_many :attribute_categories
|
||||
has_many :attribute_values, class_name: 'Attribute', dependent: :destroy
|
||||
|
||||
# Alternative approach to #content for benchmarking in prod
|
||||
def content_with_multiple_queries(
|
||||
content_types: Rails.application.config.content_type_names[:all],
|
||||
page_scoping: { user_id: self.id },
|
||||
universe_id: nil
|
||||
)
|
||||
@content_by_page_type = {}
|
||||
content_types.each do |content_type|
|
||||
type_specific_fields = ContentPage.polymorphic_content_fields
|
||||
type_specific_fields.push 'universe_id' unless content_type == 'Universe'
|
||||
|
||||
pages_of_this_type = content_type.constantize
|
||||
.where(page_scoping)
|
||||
.select(type_specific_fields)
|
||||
|
||||
if content_type != 'Universe' && universe_id.present?
|
||||
pages_of_this_type = pages_of_this_type.where(universe_id: universe_id)
|
||||
end
|
||||
|
||||
@content_by_page_type[content_type] = []
|
||||
pages_of_this_type.each do |page_data|
|
||||
@content_by_page_type[content_type].push ContentPage.new(page_data.attributes)
|
||||
end
|
||||
end
|
||||
|
||||
@content_by_page_type
|
||||
end
|
||||
|
||||
# {
|
||||
# characters: [...],
|
||||
# locations: [...]
|
||||
# }
|
||||
def content(
|
||||
content_types: Rails.application.config.content_types[:all].map(&:name),
|
||||
content_types: Rails.application.config.content_type_names[:all],
|
||||
page_scoping: { user_id: self.id },
|
||||
universe_id: nil
|
||||
)
|
||||
return {} if content_types.empty?
|
||||
# return content_with_multiple_queries(content_types: content_types, page_scoping: page_scoping, universe_id: universe_id)
|
||||
|
||||
polymorphic_content_fields = [:id, :name, :page_type, :user_id, :created_at, :updated_at, :deleted_at, :archived_at, :privacy]
|
||||
return {} if content_types.empty?
|
||||
# TODO: we should return early if we already have @content_by_page_type!!!
|
||||
|
||||
polymorphic_content_fields = ContentPage.polymorphic_content_fields
|
||||
where_conditions = page_scoping.map { |key, value| "#{key} = #{value}" }.join(' AND ') + ' AND deleted_at IS NULL AND archived_at IS NULL'
|
||||
|
||||
sql = content_types.uniq.map do |page_type|
|
||||
@ -67,7 +98,7 @@ module HasContent
|
||||
)
|
||||
|
||||
# todo we can't select for universe_id here which kind of sucks, so we need to research 1) the repercussions, 2) what to do instead
|
||||
polymorphic_content_fields = [:id, :name, :page_type, :user_id, :created_at, :updated_at, :deleted_at, :archived_at, :privacy]
|
||||
polymorphic_content_fields = ContentPage.polymorphic_content_fields
|
||||
where_conditions = page_scoping.map { |key, value| "#{key} = #{value}" }.join(' AND ') + ' AND deleted_at IS NULL AND archived_at IS NULL'
|
||||
|
||||
sql = content_types.uniq.map do |page_type|
|
||||
|
||||
@ -1,9 +1,16 @@
|
||||
class ContentPage < ApplicationRecord
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :universe
|
||||
|
||||
attr_accessor :favorite
|
||||
|
||||
include Authority::Abilities
|
||||
self.authorizer_name = 'ContentPageAuthorizer'
|
||||
|
||||
def random_image_including_private(format: :small)
|
||||
ImageUpload.where(content_type: self.page_type, content_id: self.id).sample.try(:src, format) || "card-headers/#{self.page_type.downcase.pluralize}.jpg"
|
||||
ImageUpload.where(content_type: self.page_type, content_id: self.id).sample.try(:src, format) || "/assets/card-headers/#{self.page_type.downcase.pluralize}.jpg"
|
||||
end
|
||||
|
||||
def icon
|
||||
@ -17,4 +24,20 @@ class ContentPage < ApplicationRecord
|
||||
def text_color
|
||||
self.page_type.constantize.text_color
|
||||
end
|
||||
|
||||
def favorite?
|
||||
!!favorite
|
||||
end
|
||||
|
||||
def view_path
|
||||
send("#{self.page_type.downcase}_path", self.id)
|
||||
end
|
||||
|
||||
def edit_path
|
||||
send("edit_#{self.page_type.downcase}_path", self.id)
|
||||
end
|
||||
|
||||
def self.polymorphic_content_fields
|
||||
[:id, :name, :favorite, :page_type, :user_id, :created_at, :updated_at, :deleted_at, :archived_at, :privacy]
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
class TimelineEvent < ApplicationRecord
|
||||
acts_as_paranoid
|
||||
|
||||
belongs_to :timeline
|
||||
belongs_to :timeline, touch: true
|
||||
|
||||
has_many :timeline_event_entities, dependent: :destroy
|
||||
|
||||
|
||||
@ -119,10 +119,6 @@ class User < ApplicationRecord
|
||||
@cached_user_contributable_universes ||= Universe.where(id: contributable_universe_ids)
|
||||
end
|
||||
|
||||
def linkable_universes
|
||||
@cached_linkable_universes ||= Universe.where(id: my_universe_ids + contributable_universes)
|
||||
end
|
||||
|
||||
def contributable_universe_ids
|
||||
# TODO: email confirmation needs to happen for data safety / privacy (only verified emails)
|
||||
@contributable_universe_ids ||= Contributor.where('email = ? OR user_id = ?', self.email, self.id).pluck(:universe_id)
|
||||
@ -156,7 +152,7 @@ class User < ApplicationRecord
|
||||
universe_id IN (#{(my_universe_ids + contributable_universe_ids + [-1]).uniq.join(',')})
|
||||
OR
|
||||
(universe_id IS NULL AND user_id = #{self.id.to_i})
|
||||
""")
|
||||
""").includes([:user])
|
||||
end
|
||||
|
||||
def linkable_timelines
|
||||
|
||||
@ -15,4 +15,13 @@ class FieldTypeService < Service
|
||||
raise "Unexpected/unhandled field type: #{field[:type]}"
|
||||
end
|
||||
end
|
||||
|
||||
def self.form_path_from_attribute_field(field)
|
||||
field_data = {
|
||||
type: field.field_type,
|
||||
internal_id: field.id
|
||||
}
|
||||
|
||||
form_path(field_data)
|
||||
end
|
||||
end
|
||||
|
||||
@ -5,7 +5,7 @@ class PermissionService < Service
|
||||
end
|
||||
|
||||
def self.user_owns_content?(user:, content:)
|
||||
content.user && user && content.user.try(:id) == user.try(:id)
|
||||
content.user && user && content.try(:user_id) == user.try(:id)
|
||||
end
|
||||
|
||||
def self.user_owns_any_containing_universe?(user:, content:)
|
||||
@ -27,7 +27,11 @@ class PermissionService < Service
|
||||
def self.user_can_contribute_to_containing_universe?(user:, content:)
|
||||
return false if user.nil?
|
||||
return true if [AttributeCategory, AttributeField, Attribute].include?(content.class) #todo audit this
|
||||
content.universe.present? && user.contributable_universes.pluck(:id).include?(content.universe.id)
|
||||
|
||||
return true if user.contributable_universe_ids.include?(content.universe_id)
|
||||
return true if user.universes.pluck(:id).include?(content.universe_id)
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
def self.content_has_no_containing_universe?(content:)
|
||||
|
||||
@ -4,7 +4,7 @@ class SerendipitousService < Service
|
||||
|
||||
categories_for_this_type = AttributeCategory.where(
|
||||
user: content.user,
|
||||
entity_type: content.class.name.downcase,
|
||||
entity_type: content.page_type.downcase,
|
||||
hidden: [nil, false]
|
||||
)
|
||||
|
||||
@ -25,7 +25,7 @@ class SerendipitousService < Service
|
||||
#raise fields_for_these_categories.pluck(:label).inspect
|
||||
|
||||
attribute_fields_with_values = Attribute.where(
|
||||
entity_type: content.class.name,
|
||||
entity_type: content.page_type,
|
||||
entity_id: content.id,
|
||||
attribute_field_id: fields_for_these_categories.pluck(:id)
|
||||
).where.not(
|
||||
|
||||
@ -11,6 +11,7 @@ class TemporaryFieldMigrationService < Service
|
||||
def self.migrate_fields_for_content(content_model, user, force: false)
|
||||
return unless content_model.present? && user.present?
|
||||
return unless content_model.user == user
|
||||
return if content_model.is_a?(ContentPage)
|
||||
return if !force && content_model.persisted? && content_model.created_at > 'May 1, 2018'.to_datetime
|
||||
return if !!content_model.columns_migrated_from_old_style?
|
||||
|
||||
|
||||
@ -100,7 +100,7 @@
|
||||
<div class="card col s12 m10 offset-m1 l8 offset-l2">
|
||||
<div class="card-content">
|
||||
<div class="card-title">
|
||||
Gain full access to
|
||||
Gain full access to millions of
|
||||
<% Rails.application.config.content_types[:all_non_universe].each do |type| %>
|
||||
<span class="<%= type.text_color %>"><%= type.name.downcase.pluralize %>,</span>
|
||||
<% end %>
|
||||
@ -110,7 +110,7 @@
|
||||
<div class="col s12 m12 l6">
|
||||
<p>
|
||||
After a user authenticates your application, you'll have full access to integrate their worldbuilding pages into your app.
|
||||
You can show them their characters, let them edit one of their existing creatures, pin their towns and landmarks to your maps,
|
||||
You can show them their characters, let them import pictures from their locations, pin their towns and landmarks to your maps,
|
||||
create new items, and more — all without leaving your app.
|
||||
</p>
|
||||
<br />
|
||||
|
||||
@ -6,22 +6,25 @@
|
||||
<% if defined?(field) && field.present? %>
|
||||
<ul class="hoverable collapsible white">
|
||||
<li class="<%= 'active' if defined?(expand_by_default) && !!expand_by_default %>">
|
||||
<div class="collapsible-header <%= content.class.color %> white-text">
|
||||
<i class="material-icons tooltipped" data-tooltip="Answer this randomly-generated question to have it automatically saved to your <%= content.name_field_value %> <%= content.class.name.downcase %> page.">help</i>
|
||||
<div class="collapsible-header <%= content.color %> white-text">
|
||||
<i class="material-icons tooltipped" data-tooltip="Answer this randomly-generated question to have it automatically saved to your <%= content.name %> <%= content.page_type.downcase %> page.">help</i>
|
||||
<%=
|
||||
t(
|
||||
"serendipitous_questions.attributes.#{content.class.name.downcase}.#{field.label.downcase}",
|
||||
name: content.name_field_value,
|
||||
default: "What is #{content.name_field_value}'s #{field.label.downcase}?"
|
||||
"serendipitous_questions.attributes.#{content.page_type.downcase}.#{field.label.downcase}",
|
||||
name: content.name,
|
||||
default: "What is #{content.name}'s #{field.label.downcase}?"
|
||||
)
|
||||
%>
|
||||
</div>
|
||||
<div class="collapsible-body">
|
||||
<%= form_for content do |f| %>
|
||||
<%= form_for content, url: FieldTypeService.form_path_from_attribute_field(field), method: :patch do |f| %>
|
||||
<%= hidden_field(:override, :redirect_path, value: redirect_path) if defined?(redirect_path) %>
|
||||
|
||||
<%= hidden_field_tag "entity[entity_id]", content.id %>
|
||||
<%= hidden_field_tag "entity[entity_type]", content.page_type %>
|
||||
|
||||
<%=
|
||||
render 'content/form/text_input',
|
||||
render 'content/form/text_input_for_content_page',
|
||||
f: f,
|
||||
content: content,
|
||||
field: field
|
||||
@ -32,14 +35,14 @@
|
||||
<% end %>
|
||||
|
||||
<% if include_quick_reference %>
|
||||
<%= link_to content, class: 'entity-trigger sidenav-trigger orange white-text btn tooltipped', data: { target: "quick-reference-#{@content.class.name}-#{@content.id}", tooltip: "View this #{@content.class.name.downcase} without leaving this page" } do %>
|
||||
<%= link_to content.view_path, class: 'entity-trigger sidenav-trigger orange white-text btn tooltipped', data: { target: "quick-reference-#{content.page_type}-#{content.id}", tooltip: "View this #{content.page_type.downcase} without leaving this page" } do %>
|
||||
<i class="material-icons right">vertical_split</i>
|
||||
Quick-reference
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if !defined?(show_view_button) || !!show_view_button %>
|
||||
<%= link_to content, class: "btn #{content.class.color} white-text tooltipped", target: '_new', data: { tooltip: "View this #{@content.class.name.downcase} in a new tab" } do %>
|
||||
<i class="material-icons left white-text"><%= content.class.icon %></i>
|
||||
<%= link_to content.view_path, class: "btn #{content.color} white-text tooltipped", target: '_new', data: { tooltip: "View this #{content.name.downcase} in a new tab" } do %>
|
||||
<i class="material-icons left white-text"><%= content.icon %></i>
|
||||
View
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
</li>
|
||||
<li class="divider" tabindex="-1"></li>
|
||||
<%
|
||||
linkable_universes_with_this_kind_of_content = current_user.linkable_universes.select do |universe|
|
||||
linkable_universes_with_this_kind_of_content = @linkables_raw.fetch('Universe', []).select do |universe|
|
||||
@current_user_content.fetch(content_type.name, []).any? { |content| content.universe_id == universe.id }
|
||||
end
|
||||
%>
|
||||
|
||||
62
app/views/content/form/_text_input_for_content_page.html.erb
Normal file
62
app/views/content/form/_text_input_for_content_page.html.erb
Normal file
@ -0,0 +1,62 @@
|
||||
<%
|
||||
content_name = content.page_type.downcase
|
||||
|
||||
field_id = "#{content_name}_#{field.name}"
|
||||
value = field.attribute_values.find_by(
|
||||
user: content.user,
|
||||
entity_type: content.page_type,
|
||||
entity_id: content.id
|
||||
).try(:value)
|
||||
|
||||
should_autocomplete = defined?(autocomplete) && !!autocomplete
|
||||
should_autosave = defined?(autosave) && !!autosave
|
||||
%>
|
||||
|
||||
<div class="input-field content-field">
|
||||
<% unless defined?(show_label) && !show_label %>
|
||||
<%= f.label field.id do %>
|
||||
<%= field.label.present? ? field.label : ' ' %>
|
||||
<% if defined?(autocomplete) && autocomplete %>
|
||||
<i class="material-icons grey-text lighten-2 tooltipped" style="font-size: 100%" data-tooltip="This field may suggest some ideas for you when you start typing." data-position="right">
|
||||
offline_bolt
|
||||
</i>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%
|
||||
placeholder = I18n.translate "attributes.#{content_name}.#{field.label.downcase.gsub(/\s/, '_')}",
|
||||
scope: :serendipitous_questions,
|
||||
name: content.name || "this #{content_name}",
|
||||
default: 'Write as little or as much as you want!'
|
||||
%>
|
||||
|
||||
<%= hidden_field_tag "field[name]", field[:id] %>
|
||||
<%=
|
||||
text_area_tag "field[value]",
|
||||
value,
|
||||
class: "js-can-mention-pages materialize-textarea" \
|
||||
+ "#{' autocomplete js-autocomplete-' + field[:id].to_s if should_autocomplete}" \
|
||||
+ "#{' autosave-closest-form-on-change' if should_autosave}",
|
||||
placeholder: placeholder
|
||||
%>
|
||||
</div>
|
||||
|
||||
<% if defined?(autocomplete) && autocomplete %>
|
||||
<%= content_for :javascript do %>
|
||||
$(function() {
|
||||
// This setTimeout is an unfortunate hack to ensure this runs after initializing materialize
|
||||
setTimeout(function() {
|
||||
console.log("Initializing autocomplete for #<%= "#{content_name}_#{field.label}" %>");
|
||||
|
||||
$('.js-autocomplete-<%= field.id.to_s %>').autocomplete({
|
||||
limit: 5,
|
||||
data: {
|
||||
<% autocomplete.each do |autocomplete_option| %>
|
||||
"<%= autocomplete_option %>": null,
|
||||
<% end %>
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
<% end %>
|
||||
<% end %>
|
||||
@ -13,7 +13,7 @@
|
||||
<div class="col s12">
|
||||
<%= render partial: 'cards/serendipitous/content_question', locals: {
|
||||
content: @questioned_content,
|
||||
field: @attribute_field_to_question
|
||||
field: @attribute_field_to_question
|
||||
} %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@ -4,14 +4,7 @@
|
||||
<div class="hoverable card sticky-action" style="margin-bottom: 2px">
|
||||
<div class="card-image waves-effect waves-block waves-light">
|
||||
<%= render partial: 'content/display/favorite_control', locals: { content: content } %>
|
||||
<% content_image = asset_path("card-headers/#{content_type.name.downcase.pluralize}.jpg") %>
|
||||
<% if content.respond_to?(:image_uploads) %>
|
||||
<% images = content.image_uploads %>
|
||||
<% if images.any? %>
|
||||
<% content_image = images.sample.src(:medium) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<div class="activator" style="height: 265px; background: url('<%= content_image %>'); background-size: cover;"></div>
|
||||
<div class="activator" style="height: 265px; background: url('<%= content.random_image_including_private(format: :medium) %>'); background-size: cover;"></div>
|
||||
|
||||
<span class="card-title js-content-name activator">
|
||||
<div class="bordered-text">
|
||||
@ -35,13 +28,13 @@
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<% if current_user.can_update?(content) %>
|
||||
<%= link_to edit_polymorphic_path(content), class: 'green-text right', target: content.is_a?(Document) ? '_new' : '_self' do %>
|
||||
<%= link_to content.edit_path, class: 'green-text right', target: content.is_a?(Document) ? '_new' : '_self' do %>
|
||||
<i class="material-icons left"><%= content_type.icon %></i>
|
||||
Edit
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if current_user.can_read?(content) %>
|
||||
<%= link_to polymorphic_path(content), class: 'blue-text text-lighten-1' do %>
|
||||
<%= link_to content.view_path, class: 'blue-text text-lighten-1' do %>
|
||||
<i class="material-icons left"><%= content_type.icon %></i>
|
||||
View
|
||||
<% end %>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<% if @universe_scope %>
|
||||
<p class="center help-text">
|
||||
<p class="center help-text teal card-panel lighten-5 black-text">
|
||||
Only showing documents
|
||||
in the <%= link_to @universe_scope.name, @universe_scope, class: Universe.color + '-text' %> universe.
|
||||
in the <%= link_to @universe_scope.name, @universe_scope, class: Universe.text_color %> universe.
|
||||
<%= link_to(
|
||||
"See documents from all universes.",
|
||||
"See documents from all universes instead.",
|
||||
'?universe=all',
|
||||
class: Universe.color + '-text')
|
||||
class: Universe.text_color)
|
||||
%>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
@ -72,14 +72,16 @@
|
||||
your worlds
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @content %>
|
||||
<%= render partial: 'cards/serendipitous/content_question', locals: {
|
||||
content: @content,
|
||||
field: @attribute_field_to_question,
|
||||
expand_by_default: true,
|
||||
include_quick_reference: false
|
||||
} %>
|
||||
<% end %>
|
||||
<%=
|
||||
if @content
|
||||
render partial: 'cards/serendipitous/content_question', locals: {
|
||||
content: @content,
|
||||
field: @attribute_field_to_question,
|
||||
expand_by_default: true,
|
||||
include_quick_reference: false
|
||||
}
|
||||
end
|
||||
%>
|
||||
|
||||
<%= link_to prompts_path do %>
|
||||
<div class="hoverable card-panel orange white-text" style="margin: 0; margin-bottom: 2em">
|
||||
|
||||
@ -12,7 +12,8 @@
|
||||
redirect_path: prompts_path,
|
||||
show_empty_prompt: true,
|
||||
expand_by_default: true,
|
||||
show_view_button: false
|
||||
show_view_button: false,
|
||||
include_quick_reference: false
|
||||
} %>
|
||||
|
||||
<p class="grey-text text-lighten-1 center">
|
||||
@ -23,8 +24,8 @@
|
||||
|
||||
<% if @attribute_field_to_question.present? %>
|
||||
<div class="col m12 l4">
|
||||
<%= link_to @content, class: 'entity-trigger sidenav-trigger', data: { target: "quick-reference-#{@content.class.name}-#{@content.id}"} do %>
|
||||
<div class="hoverable card <%= @content.class.color %>">
|
||||
<%= link_to @content.view_path, class: 'entity-trigger sidenav-trigger', data: { target: "quick-reference-#{@content.page_type}-#{@content.id}"} do %>
|
||||
<div class="hoverable card <%= @content.color %>">
|
||||
<div class="card-content white-text">
|
||||
<i class="material-icons right">vertical_split</i>
|
||||
Quick-reference <%= @content.name %>
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
<%# TODO: put this in more of a timeline design %>
|
||||
<div class="row">
|
||||
<div class="col s12">
|
||||
<br />
|
||||
<div class="grey-text uppercase center">Your recent worldbuilding activity</div>
|
||||
</div>
|
||||
<% @recently_edited_pages.each do |page| %>
|
||||
<% action = page.created_at === page.updated_at ? 'Created' : 'Updated' %>
|
||||
<% klass = page.is_a?(ContentPage) ? content_class_from_name(page.page_type) : page.class %>
|
||||
|
||||
@ -1,16 +1,25 @@
|
||||
<%# todo extract "sidebar" and call it with @content, then also do the same in documents/components/smart_sidebar %>
|
||||
<% serialized_entity = ContentSerializer.new(content) %>
|
||||
<%#
|
||||
This sidebar partial uses instantiated ContentPage models instead of the persisted content models (Character, Location, etc)
|
||||
|
||||
<ul id="quick-reference-<%= content.class.name %>-<%= content.id %>" class="sidenav quick-reference-sidenav">
|
||||
TODO: merge this with documents/components/smart_sidebar
|
||||
%>
|
||||
|
||||
<%# todo extract "sidebar" and call it with @content, then also do the same in documents/components/smart_sidebar %>
|
||||
<%
|
||||
raw_model = content.page_type.constantize.find_by(id: content.id, user: current_user)
|
||||
serialized_entity = ContentSerializer.new(raw_model)
|
||||
%>
|
||||
|
||||
<ul id="quick-reference-<%= content.page_type %>-<%= content.id %>" class="sidenav quick-reference-sidenav">
|
||||
<li>
|
||||
<div class="user-view">
|
||||
<div class="background">
|
||||
<%= image_tag "card-headers/#{content.class.name.downcase.pluralize}.jpg", width: '100%' %>
|
||||
<%= image_tag "card-headers/#{content.page_type.downcase.pluralize}.jpg", width: '100%' %>
|
||||
</div>
|
||||
<a href="#name">
|
||||
<h1 class="white-text name bordered-text">
|
||||
<i class="material-icons <%= content.class.text_color %> left">
|
||||
<%= content.class.icon %>
|
||||
<i class="material-icons <%= content.text_color %> left">
|
||||
<%= content.icon %>
|
||||
</i>
|
||||
<%= content.name %>
|
||||
</h1>
|
||||
@ -83,15 +92,15 @@
|
||||
<li><a href="#" class="subheader">Actions</a></li>
|
||||
|
||||
<li>
|
||||
<%= link_to polymorphic_path(content), class: "blue-text", target: '_new' do %>
|
||||
<i class="material-icons left <%= content.class.text_color %>"><%= content.class.icon %></i>
|
||||
<%= link_to content.view_path, class: "blue-text", target: '_new' do %>
|
||||
<i class="material-icons left <%= content.text_color %>"><%= content.icon %></i>
|
||||
<i class="material-icons right grey-text">exit_to_app</i>
|
||||
View <%= content.name %>
|
||||
<% end %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to edit_polymorphic_path(content), class: "green-text" do %>
|
||||
<i class="material-icons left <%= content.class.text_color %>"><%= content.class.icon %></i>
|
||||
<%= link_to content.edit_path, class: "green-text" do %>
|
||||
<i class="material-icons left <%= content.text_color %>"><%= content.icon %></i>
|
||||
<i class="material-icons right grey-text">exit_to_app</i>
|
||||
Edit <%= content.name %>
|
||||
<% end %>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user