Convert attribute_categories to system categories

This commit is contained in:
Matthew Werner 2016-10-10 14:47:51 -07:00
parent d0f33cbbb0
commit 5a3cfcb6d1
43 changed files with 820 additions and 246 deletions

View File

@ -0,0 +1,15 @@
# Controller for the Attribute model
class AttributeCategoriesController < ContentController
private
def content_params
params.require(:attribute_category).permit(content_param_list)
end
def content_param_list
[
:user_id, :entity_type,
:name, :label, :icon, :description
]
end
end

View File

@ -0,0 +1,17 @@
# Controller for the Attribute model
class AttributeFieldsController < ContentController
private
def content_params
params.require(:attribute_field).permit(content_param_list)
end
def content_param_list
[
:universe_id, :user_id,
:attribute_category_id,
:name, :field_type,
:label, :description
]
end
end

View File

@ -17,6 +17,7 @@ class CharactersController < ContentController
:mannerisms, :birthday, :education, :background,
:fave_color, :fave_food, :fave_possession, :fave_weapon, :fave_animal,
:notes, :private_notes, :privacy,
custom_attributes: [:name, :value],
siblingships_attributes: [:id, :sibling_id, :_destroy],
fatherships_attributes: [:id, :father_id, :_destroy],
motherships_attributes: [:id, :mother_id, :_destroy],

View File

@ -20,7 +20,7 @@ module HasOwnership
model = content_type_from_controller(self.class)
redirect_if_not_owned model.find(params[:id]), send(redirect_path)
rescue
redirect_to '/500'
redirect_to '/500' unless Rails.env.development?
end
# Unless this content is shared, ensure only the owner can do this action

View File

@ -23,8 +23,10 @@ class ContentController < ApplicationController
def show
# TODO: Secure this with content class whitelist lel
@content = content_type_from_controller(self.class).find(params[:id])
content_type = content_type_from_controller(self.class)
@content = content_type.find(params[:id])
@question = @content.question if current_user.present? and current_user == @content.user
@attribute_categories = current_user.attribute_categories.joins(:attribute_fields).where(['attribute_categories.entity_type = ?', content_type])
respond_to do |format|
format.html # show.html.erb
@ -33,8 +35,9 @@ class ContentController < ApplicationController
end
def new
@content = content_type_from_controller(self.class)
.new
content_type = content_type_from_controller(self.class)
@content = content_type.new
@attribute_categories = current_user.attribute_categories.joins(:attribute_fields).where(['attribute_categories.entity_type = ?', content_type])
respond_to do |format|
format.html # new.html.erb
@ -43,8 +46,9 @@ class ContentController < ApplicationController
end
def edit
@content = content_type_from_controller(self.class)
.find(params[:id])
content_type = content_type_from_controller(self.class)
@content = content_type.find(params[:id])
@attribute_categories = current_user.attribute_categories.joins(:attribute_fields).where(['attribute_categories.entity_type = ?', content_type])
end
def create
@ -81,6 +85,7 @@ class ContentController < ApplicationController
def initialize_object
content_type = content_type_from_controller(self.class)
@content = content_type.new(content_params).tap do |c|
c.user_id = current_user.id
end

8
app/models/attribute.rb Normal file
View File

@ -0,0 +1,8 @@
class Attribute < ActiveRecord::Base
belongs_to :user
belongs_to :attribute_field
belongs_to :entity, polymorphic: true
include HasPrivacy
scope :is_public, -> { eager_load(:universe).where('universes.privacy = ? OR attributes.privacy = ?', 'public', 'public') }
end

View File

@ -0,0 +1,27 @@
class AttributeCategory < ActiveRecord::Base
belongs_to :user
has_many :attribute_fields
include Serendipitous::Concern
def self.color
'amber'
end
def self.icon
'chrome_reader_mode'
end
def self.content_name
'attribute category'
end
def self.attribute_categories
{
general: {
icon: 'info',
attributes: %w(name label icon entity_type)
}
}
end
end

View File

@ -0,0 +1,38 @@
class AttributeField < ActiveRecord::Base
belongs_to :user
include HasAttributes
include Serendipitous::Concern
attr_accessor :system
scope :is_public, -> { eager_load(:universe).where('universes.privacy = ? OR attribute_fields.privacy = ?', 'public', 'public') }
def self.color
'amber'
end
def self.icon
'text_fields'
end
def self.content_name
'attribute'
end
def name
(self['name'] || "custom field #{Time.now.to_i}").downcase.gsub(' ','_')
end
def humanize
label
end
def private?
privacy != 'public'
end
def system?
!!self.system
end
end

View File

@ -14,6 +14,7 @@ class Character < ActiveRecord::Base
belongs_to :universe
include HasAttributes
include HasPrivacy
include HasContentGroupers
include Serendipitous::Concern
@ -33,12 +34,18 @@ class Character < ActiveRecord::Base
# Items
relates :favorite_items, with: :ownerships, where: { favorite: true }
has_many :custom_attributes
scope :is_public, -> { eager_load(:universe).where('characters.privacy = ? OR universes.privacy = ?', 'public', 'public') }
def description
role
end
def self.content_name
'character'
end
def self.color
'red'
end
@ -46,38 +53,4 @@ class Character < ActiveRecord::Base
def self.icon
'group'
end
def self.attribute_categories
{
overview: {
icon: 'info',
attributes: %w(name role gender age universe_id)
},
looks: {
icon: 'face',
attributes: %w(weight height haircolor hairstyle facialhair eyecolor race skintone bodytype identmarks)
},
social: {
icon: 'groups',
attributes: %w(best_friends archenemies religion politics prejudices occupation)
},
# TODO: remove schema for mannerisms
history: {
icon: 'info',
attributes: %w(birthday birthplaces education background)
},
faves: {
icon: 'star',
attributes: %w(fave_color fave_food fave_possession fave_weapon fave_animal)
},
family: {
icon: 'face',
attributes: %w(mothers fathers spouses siblings children)
},
notes: {
icon: 'edit',
attributes: %w(notes private_notes)
}
}
end
end

View File

@ -0,0 +1,27 @@
require 'active_support/concern'
module HasAttributes
extend ActiveSupport::Concern
included do
attr_accessor :attribute_values
after_save :update_attribute_values
def self.attribute_categories
YAML.load_file(Rails.root.join('config', 'attributes', "#{content_name}.yml")).map do |category_name, details|
category = AttributeCategory.new(entity_type: self.name, name: category_name.to_s, label: details[:label], icon: details[:icon])
category.attribute_fields << details[:attributes].map { |field| AttributeField.new(field.merge(system: true)) }
category
end
end
def update_attribute_values
(self.attribute_values || []).each do |attribute|
field = user.attribute_fields.find_by_name(attribute[:name])
next if field.nil?
user.custom_attributes.where(entity: self, attribute_field_id: field.id).first_or_create(value: attribute[:value])
end
end
end
end

View File

@ -12,6 +12,7 @@ class Item < ActiveRecord::Base
belongs_to :user
belongs_to :universe
include HasAttributes
include HasPrivacy
include HasContentGroupers
include Serendipitous::Concern
@ -31,28 +32,7 @@ class Item < ActiveRecord::Base
'beach_access'
end
def self.attribute_categories
{
overview: {
icon: 'info',
attributes: %w(name item_type description universe_id)
},
looks: {
icon: 'redeem',
attributes: %w(materials weight)
},
history: {
icon: 'book',
attributes: %w(original_owners current_owners makers year_made)
},
abilities: {
icon: 'flash_on',
attributes: %w(magic)
},
notes: {
icon: 'edit',
attributes: %w(notes private_notes)
}
}
def self.content_name
'item'
end
end

View File

@ -15,6 +15,7 @@ class Location < ActiveRecord::Base
belongs_to :user
belongs_to :universe
include HasAttributes
include HasPrivacy
include HasContentGroupers
include Serendipitous::Concern
@ -37,33 +38,7 @@ class Location < ActiveRecord::Base
'green'
end
def self.attribute_categories
{
overview: {
icon: 'info',
attributes: %w(name type_of description universe_id)
},
# TODO: map
culture: {
icon: 'face',
attributes: %w(leaders population language currency motto)
},
cities: {
icon: 'business',
attributes: %w(capital_cities largest_cities notable_cities)
},
geography: {
icon: 'map',
attributes: %w(area crops located_at)
},
history: {
icon: 'book',
attributes: %w(established_year notable_wars)
},
notes: {
icon: 'edit',
attributes: %w(notes private_notes)
}
}
def self.content_name
'location'
end
end

View File

@ -6,6 +6,8 @@
#
# contains all canonically-related content created by Users
class Universe < ActiveRecord::Base
include HasAttributes
include HasPrivacy
include Serendipitous::Concern
@ -15,6 +17,7 @@ class Universe < ActiveRecord::Base
has_many :characters
has_many :items
has_many :locations
has_many :attribute_fields
scope :is_public, -> { where(privacy: 'public') }
@ -34,24 +37,7 @@ class Universe < ActiveRecord::Base
'vpn_lock'
end
def self.attribute_categories
{
overview: {
icon: 'info',
attributes: %w(name description)
},
history: {
icon: 'book',
attributes: %w(history)
},
notes: {
icon: 'edit',
attributes: %w(notes private_notes)
},
settings: {
icon: 'build',
attributes: %w(privacy)
}
}
def self.content_name
'universe'
end
end

View File

@ -15,6 +15,9 @@ class User < ActiveRecord::Base
has_many :items
has_many :locations
has_many :universes
has_many :attribute_fields
has_many :attribute_categories
has_many :custom_attributes, class_name: 'Attribute'
# as_json creates a hash structure, which you then pass to ActiveSupport::json.encode to actually encode the object as a JSON string.
# This is different from to_json, which converts it straight to an escaped JSON string,

View File

@ -0,0 +1,20 @@
<div class="row">
<div class="col s12">
<h1>Attributes</h1>
</div>
</div>
<div class="row">
<div class="col s4">
<h2>Characters per user</h1>
<%= column_chart User.joins(:attribute_fields).group(:user_id).count().group_by { |n| n.last }.each_with_object({}) { |(content_count, ids), h| h[content_count] = ids.count } %>
</div>
<div class="col s4">
<h2>Attributes per universe</h1>
<%= column_chart Universe.joins(:attribute_fields).group(:universe_id).count().group_by { |n| n.last }.each_with_object({}) { |(content_count, ids), h| h[content_count] = ids.count } %>
</div>
<div class="col s4">
<h2>Attribute privacy</h1>
<%= pie_chart AttributeField.where.not(privacy: "").group(:privacy).count() %>
</div>
</div>

View File

@ -0,0 +1,31 @@
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Entity</th>
<th>Description</th>
<% if session[:user] %>
<th><%=t '.actions', :default => t("helpers.actions") %></th>
<% end %>
</tr>
</thead>
<tbody>
<% @content.each do |category| %>
<tr>
<td style="width: 160px;"><%= simple_format link_to category.label, attribute_category_path(category) %></td>
<td><%= simple_format category.entity_type.capitalize %></td>
<td><%= simple_format category.description %></td>
<td style="width: 240px;">
<% if session[:user] and session[:user] == category.user.id %>
<%= link_to t('.view', :default => t("helpers.links.view")),
attribute_field_path(category), :class => 'btn' %>
<%= link_to t('.edit', :default => t("helpers.links.edit")),
attribute_field_edit_path(category), :class => 'btn' %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>

View File

@ -0,0 +1,3 @@
<%= form_for @content do |form| %>
<%= render partial: 'content/form', locals: { f: form, content: @content } %>
<% end %>

View File

@ -0,0 +1,3 @@
<%= form_for @content do |form| %>
<%= render partial: 'content/form', locals: { f: form, content: @content } %>
<% end %>

View File

@ -0,0 +1,12 @@
<script type="application/ld+json">
<%
content_jsonld = {
'@id': item_url,
'@type': 'http://schema.org/Thing',
'http://schema.org/name': @content.name
}
%>
<%= content_jsonld.to_json.html_safe %>
</script>
<%= render partial: 'content/show', locals: { content: @content } %>

View File

@ -0,0 +1,31 @@
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<% if session[:user] %>
<th><%=t '.actions', :default => t("helpers.actions") %></th>
<% end %>
</tr>
</thead>
<tbody>
<% @content.each do |field| %>
<tr>
<td style="width: 160px;"><%= simple_format link_to field.label, attribute_field_path(field) %></td>
<td><%= simple_format field.field_type %></td>
<td><%= simple_format field.description %></td>
<td style="width: 240px;">
<% if session[:user] and session[:user] == field.user.id %>
<%= link_to t('.view', :default => t("helpers.links.view")),
attribute_field_path(field), :class => 'btn' %>
<%= link_to t('.edit', :default => t("helpers.links.edit")),
attribute_field_edit_path(field), :class => 'btn' %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>

View File

@ -0,0 +1,3 @@
<%= form_for @content do |form| %>
<%= render partial: 'content/form', locals: { f: form, content: @content } %>
<% end %>

View File

@ -0,0 +1,3 @@
<%= form_for @content do |form| %>
<%= render partial: 'content/form', locals: { f: form, content: @content } %>
<% end %>

View File

@ -0,0 +1,12 @@
<script type="application/ld+json">
<%
content_jsonld = {
'@id': item_url,
'@type': 'http://schema.org/Thing',
'http://schema.org/name': @content.label
}
%>
<%= content_jsonld.to_json.html_safe %>
</script>
<%= render partial: 'content/show', locals: { content: @content } %>

View File

@ -15,129 +15,36 @@
<%# Tabs %>
<ul class="hoverable tabs hide-on-small-only">
<% content.class.attribute_categories.each do |category, data| %>
<% content.class.attribute_categories.each do |category| %>
<li class="tab col s12">
<a href="#<%= category %>">
<i class="material-icons hide-on-med-and-down" style="font-size: 18px; position: relative; top: 3px;"><%= data[:icon] %></i>
<%= category.to_s.humanize %>
<a href="#<%= category.name %>_panel">
<i class="material-icons hide-on-med-and-down" style="font-size: 18px; position: relative; top: 3px;"><%= category.icon %></i>
<%= category.label %>
</a>
</li>
<% end %>
</ul>
<ul class="tabs hide-on-med-and-up">
<% content.class.attribute_categories.each do |category, data| %>
<% content.class.attribute_categories.each do |category| %>
<li class="tab col s12">
<a href="#<%= category %>" class="tooltipped" data-position="bottom" data-delay="100" data-tooltip="<%= category.to_s.humanize %>">
<i class="material-icons" style="font-size: 18px; position: relative; top: 3px;"><%= data[:icon] %></i>
<a href="#<%= category.name %>_panel" class="tooltipped" data-position="bottom" data-delay="100" data-tooltip="<%= category.label %>">
<i class="material-icons" style="font-size: 18px; position: relative; top: 3px;"><%= category.icon %></i>
</a>
</li>
<% end %>
</ul>
<br />
<%# Content panels %>
<% content.class.attribute_categories.each do |category, data| %>
<div id="<%= category %>" class="row">
<% data[:attributes].each do |attribute| %>
<% # Do some dynamic resizing of columns based on how many columns there are
# TODO: move this into some service or something? Dunno, doesn't belong here.
s_width = 12
m_width = 6
l_width = 4
if data[:attributes].length == 1
# If there's only one field on this tab, go full-width on all screen sizes
s_width = m_width = l_width = 12
elsif data[:attributes].length == 2
# If there's two fields on this tab, go half-width on medium- and large-screens
s_width = 12
m_width = l_width = 6
elsif data[:attributes].length > 2
# If there's at least 3 fields, use the defaults (detailed above)
end
%>
<div class="col <%= "s#{s_width} m#{m_width} l#{l_width}" %>">
<% value = content.send(attribute) %>
<% if value.is_a?(ActiveRecord::Associations::CollectionProxy) %>
<%# Relation-setting UI %>
<% through_class = content.class.reflect_on_association(attribute).options[:through].to_s %>
<%= render 'content/form/relation_input', f: f, attribute: attribute, relation: through_class %>
<% elsif attribute == 'universe_id' %>
<div class="input-field">
<%= f.label attribute %><br />
<%= f.select attribute, current_user.universes.sort_by(&:name).map { |u| [u.name, u.id] }.compact, include_blank: true, selected: (f.object.persisted? ? f.object.universe_id : session[:universe_id]) %>
</div>
<% elsif attribute == 'privacy' %>
<div class="input-field">
<%= f.label attribute %><br />
<%= f.select attribute, [['Only visible to you', 'private'], ['Visible to anyone with the URL', 'public']] %>
<div class="help-text">
This setting applies to this universe and everything within it. If this universe is visible to others by URL, they will be able to click through and see the
characters, locations, and items within this universe also.
</div>
</div>
<% elsif attribute == 'private_notes' %>
<div class="input-field content-field">
<%= f.label attribute, attribute.humanize.capitalize %>
<%= f.text_area attribute, class: "materialize-textarea", placeholder: 'Write as little or as much as you want!' %>
<div class="help-text">
Private notes are <em>always</em> visible to only you, even if content is made public and shared.
</div>
</div>
<% elsif attribute == 'item_type' %>
<% potential_item_types = t('weapon_types') + t('shield_types') + t('axe_types') + t('bow_types') +
t('club_types') + t('flexible_weapon_types') + t('fist_weapon_types') + t('thrown_weapon_types') +
t('polearm_types') + t('shortsword_types') + t('sword_types') + t('other_item_types')
%>
<%= render 'content/form/text_input', f: f, attribute: attribute, autocomplete: potential_item_types %>
<%# TODO: not make this so awful %>
<% elsif attribute == 'skintone' %>
<%= render 'content/form/text_input', f: f, attribute: attribute, autocomplete: t('skin_tones') %>
<% elsif attribute == 'race' %>
<%= render 'content/form/text_input', f: f, attribute: attribute, autocomplete: t('character_races') %>
<% elsif attribute == 'eyecolor' %>
<%= render 'content/form/text_input', f: f, attribute: attribute, autocomplete: t('eye_colors') %>
<% elsif attribute == 'haircolor' %>
<%= render 'content/form/text_input', f: f, attribute: attribute, autocomplete: t('hair_colors') %>
<% elsif attribute == 'hairstyle' %>
<%= render 'content/form/text_input', f: f, attribute: attribute, autocomplete: t('hair_styles') %>
<% elsif attribute == 'facialhair' %>
<%= render 'content/form/text_input', f: f, attribute: attribute, autocomplete: t('facial_hair_styles') %>
<% elsif attribute == 'bodytype' %>
<%= render 'content/form/text_input', f: f, attribute: attribute, autocomplete: t('body_types') %>
<% elsif attribute == 'fave_weapon' %>
<%= render 'content/form/text_input', f: f, attribute: attribute, autocomplete: t('weapon_types') + t('shield_types') + t('axe_types') + t('bow_types') +
t('club_types') + t('flexible_weapon_types') + t('fist_weapon_types') + t('thrown_weapon_types') +
t('polearm_types') + t('shortsword_types') + t('sword_types') %>
<% else %>
<%= render 'content/form/text_input', f: f, attribute: attribute %>
<% end %>
</div>
<% end %>
</div>
<%= render 'content/form/panel', category: category, f: f, content: content %>
<% end %>
</div>
<div class="card-action center">
<%= f.submit nil, class: "btn waves-effect waves-#{content.class.color}" %>
<% action = content.new_record? ? 'Create' : 'Update' %>
<%= f.submit "#{action} #{content.class.content_name}", class: "btn waves-effect waves-#{content.class.color}" %>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
<% if @content.present? && @content.respond_to?(:as_jsonld) %>
<script type="application/ld+json">
<%= @content.as_jsonld.to_json.html_safe %>
<%= @content.as_jsonld.to_json.html_safe %>
</script>
<% end %>
@ -8,8 +8,6 @@
set_meta_tags title: content.name, description: content.description
%>
<% content_for :sidebar_top do %>
<%= render partial: 'cards/serendipitous/content_question', locals: { question: @question, content: @content } %>
<% end %>
@ -24,19 +22,19 @@ set_meta_tags title: content.name, description: content.description
<%= content.name %>
</div>
<ul class="tabs" style="margin-bottom:30px;">
<% content.class.attribute_categories.each do |category, data| %>
<li class="tab col s3 <%= "disabled" unless data[:attributes].any? { |m| content.send(m).present? } %>"><a href="#<%= category %>"><%= category.to_s.humanize %></a></li>
<% content.class.attribute_categories.each do |category| %>
<li class="tab col s3 <%= "disabled" unless category.attribute_fields.map(&:name).any? { |m| content.send(m).present? } %>"><a href="#<%= category.name %>_panel"><%= category.label %></a></li>
<% end %>
</ul>
<% content.class.attribute_categories.each do |category, data| %>
<div id="<%= category %>" class="row">
<% data[:attributes].each do |attribute| %>
<% next if attribute.start_with?("private") && @content.user != current_user %>
<% next if content.send(attribute).blank? %>
<div class="row">
<div class="col s3 m3 l2 right-align flow-text grey-text"><%= attribute.humanize.capitalize %></div>
<% content.class.attribute_categories.each do |category| %>
<div id="<%= category.name %>_panel" class="row">
<% category.attribute_fields.each do |attribute| %>
<% next if attribute.name.start_with?("private") && @content.user != current_user %>
<% value = content.send(attribute.name) %>
<% next if value.blank? %>
<% value = content.send(attribute) %>
<div class="row">
<div class="col s3 m3 l2 right-align flow-text grey-text"><%= attribute.label %></div>
<% if value.is_a?(ActiveRecord::Associations::CollectionProxy) %>
<% klass = value.first.class || value.build.class %>
<div class="col s9 m9 l10 flow-text">
@ -49,7 +47,7 @@ set_meta_tags title: content.name, description: content.description
</div>
<% end %>
</div>
<% elsif attribute == 'universe_id' %>
<% elsif attribute.name == 'universe_id' %>
<div class="col s9 m9 l10 flow-text">
<%= link_to content.universe.name, content.universe if content.universe %>
</div>

View File

@ -0,0 +1,107 @@
<div id="<%= category.name %>_panel" class="row">
<% category.attribute_fields.each do |field| %>
<% # Do some dynamic resizing of columns based on how many columns there are
# TODO: move this into some service or something? Dunno, doesn't belong here.
s_width = 12
m_width = 6
l_width = 4
if category.attribute_fields.length == 1
# If there's only one field on this tab, go full-width on all screen sizes
s_width = m_width = l_width = 12
elsif category.attribute_fields.length == 2
# If there's two fields on this tab, go half-width on medium- and large-screens
s_width = 12
m_width = l_width = 6
elsif category.attribute_fields.length > 2
# If there's at least 3 fields, use the defaults (detailed above)
end
%>
<div class="col <%= "s#{s_width} m#{m_width} l#{l_width}" %>">
<% value = content.send(field.name.to_sym) %>
<% if value.is_a?(ActiveRecord::Associations::CollectionProxy) %>
<%# Relation-setting UI %>
<% through_class = content.class.reflect_on_association(field.name).options[:through].to_s %>
<%= render 'content/form/relation_input', f: f, attribute: field.name, relation: through_class %>
<% elsif field.name == 'universe_id' %>
<div class="input-field">
<%= f.label field.name, field.label %><br />
<%= f.select field.name, current_user.universes.sort_by(&:name).map { |u| [u.name, u.id] }.compact, include_blank: true, selected: (f.object.persisted? ? f.object.universe_id : session[:universe_id]) %>
</div>
<% elsif field.name == 'attribute_category_id' %>
<div class="input-field">
<%= f.label field.name, field.label %><br />
<%= f.select field.name, current_user.attribute_categories.sort_by(&:label).map { |u| [u.label, u.id] }.compact, include_blank: false, selected: (f.object.persisted? ? f.object.attribute_category_id : nil) %>
</div>
<% elsif field.name == 'field_type' %>
<div class="input-field">
<%= f.label field.name, field.label %><br />
<%= f.select field.name, ['Text Field'].map { |tf| [tf, tf.underscore] } %>
</div>
<% elsif field.name == 'privacy' %>
<div class="input-field">
<%= f.label field.name, field.label %><br />
<%= f.select field.name, [['Only visible to you', 'private'], ['Visible to anyone with the URL', 'public']] %>
<div class="help-text">
This setting applies to this universe and everything within it. If this universe is visible to others by URL, they will be able to click through and see the
characters, locations, and items within this universe also.
</div>
</div>
<% elsif field.name == 'private_notes' %>
<div class="input-field content-field">
<%= f.label field.name, field.label %>
<%= f.text_area field.name, class: "materialize-textarea", placeholder: 'Write as little or as much as you want!' %>
<div class="help-text">
Private notes are <em>always</em> visible to only you, even if content is made public and shared.
</div>
</div>
<% elsif field.name == 'item_type' %>
<% potential_item_types = t('weapon_types') + t('shield_types') + t('axe_types') + t('bow_types') +
t('club_types') + t('flexible_weapon_types') + t('fist_weapon_types') + t('thrown_weapon_types') +
t('polearm_types') + t('shortsword_types') + t('sword_types') + t('other_item_types')
%>
<%= render 'content/form/text_input', f: f, content: content, field: field, autocomplete: potential_item_types %>
<%# TODO: not make this so awful %>
<% elsif field.name == 'skintone' %>
<%= render 'content/form/text_input', f: f, content: content, field: field, autocomplete: t('skin_tones') %>
<% elsif field.name == 'race' %>
<%= render 'content/form/text_input', f: f, content: content, field: field, autocomplete: t('character_races') %>
<% elsif field.name == 'eyecolor' %>
<%= render 'content/form/text_input', f: f, content: content, field: field, autocomplete: t('eye_colors') %>
<% elsif field.name == 'haircolor' %>
<%= render 'content/form/text_input', f: f, content: content, field: field, autocomplete: t('hair_colors') %>
<% elsif field.name == 'hairstyle' %>
<%= render 'content/form/text_input', f: f, content: content, field: field, autocomplete: t('hair_styles') %>
<% elsif field.name == 'facialhair' %>
<%= render 'content/form/text_input', f: f, content: content, field: field, autocomplete: t('facial_hair_styles') %>
<% elsif field.name == 'bodytype' %>
<%= render 'content/form/text_input', f: f, content: content, field: field, autocomplete: t('body_types') %>
<% elsif field.name == 'fave_weapon' %>
<%= render 'content/form/text_input', f: f, content: content, field: field, autocomplete: t('weapon_types') + t('shield_types') + t('axe_types') + t('bow_types') +
t('club_types') + t('flexible_weapon_types') + t('fist_weapon_types') + t('thrown_weapon_types') +
t('polearm_types') + t('shortsword_types') + t('sword_types') %>
<% else %>
<%= render 'content/form/text_input', f: f, content: content, field: field %>
<% end %>
</div>
<% end %>
</div>

View File

@ -1,19 +1,23 @@
<%
field_id = "#{f.object.class.name.downcase}_#{attribute}"
value = content.send(field.name.to_sym)
# TODO: Enable autocomplete when we can actually hide the dropdown if someone tabs out of a field.
# Not the easiest thing in the world to do, apparently.
%>
<div class="input-field content-field">
<%= f.label attribute, attribute.humanize.capitalize %>
<%= f.text_area attribute, class: "materialize-textarea #{defined?(autocomplete) && false ? 'autocomplete' : ''}", placeholder: 'Write as little or as much as you want!' %>
<%= f.label field.name, field.label %>
<% if field.system? %>
<%= f.text_area field.name, value: value, class: "materialize-textarea #{defined?(autocomplete) && false ? 'autocomplete' : ''}", placeholder: 'Write as little or as much as you want!' %>
<% else %>
<%= hidden_field_tag "#{content.content_name}[custom_attributes][][name]", field.name %>
<%= text_area_tag "#{content.content_name}[custom_attributes][][value]", value, class: "materialize-textarea #{defined?(autocomplete) && false ? 'autocomplete' : ''}", placeholder: 'Write as little or as much as you want!' %>
<% end %>
</div>
<% if defined?(autocomplete) && false %>
<script type="text/javascript">
$(function() {
$('#<%= field_id %>').autocomplete({
$('#<%= "#{content.content_name}_#{field.name}" %>').autocomplete({
data: {
<% autocomplete.each do |autocomplete_option| %>
"<%= autocomplete_option %>": null,
@ -22,4 +26,4 @@
});
});
</script>
<% end %>
<% end %>

View File

@ -1,5 +1,6 @@
<% if content.persisted? %>
<%= content.name %>
<% else %>
Create your new <%= content.class.to_s.downcase %>
<% end %>
<%- content_name = content.class.respond_to?(:content_name) ? content.class.content_name: content.class.to_s %>
Create your new <%= content_name.downcase %>
<% end %>

View File

@ -1,6 +1,5 @@
<%
content_type_class = @content.build.class
content_type = content_type_class.name.downcase
%>
<% content_for :sidebar_top do %>
@ -8,22 +7,22 @@
<% end %>
<% if @content.any? %>
<h4>You've created <%= pluralize(@content.count, content_type) %></h4>
<h4>You've created <%= pluralize(@content.count, @content.content_name) %></h4>
<%= render partial: 'content/list/list', locals: { content_list: @content } %>
<%= link_to "Create another #{content_type}", new_polymorphic_path(@content.build), :class => 'btn' %>
<%= link_to "Create another #{@content.content_name}", new_polymorphic_path(@content.build), :class => 'btn' %>
<% elsif @content.empty? %>
<div class="center">
<h4>You haven't created any <%= content_type.pluralize %> yet!</h4>
<h4>You haven't created any <%= @content.content_name.pluralize %> yet!</h4>
<h1>
<i class="material-icons <%= content_type_class.color %>-text" style="font-size: 200%">
<%= content_type_class.icon %>
</i>
</h1>
<p>
<%= t("content_descriptions.#{content_type}") %>
<%= t("content_descriptions.#{content_type_class.name.downcase}") %>
</p>
<%= link_to "Create your first #{content_type}", new_polymorphic_path(@content.build), :class => 'btn' %>
<%= link_to "Create your first #{@content.content_name}", new_polymorphic_path(@content.build), :class => 'btn' %>
</div>
<% end %>

View File

@ -2,6 +2,7 @@
<% if title.present? %>
<li class="collection-header"><h4><%= title %></h4></li>
<% end %>
<% content_list.each do |content| %>
<li class="collection-item avatar">
<i class="material-icons circle <%= content.class.color %>"><%= content.class.icon %></i>
@ -39,4 +40,4 @@
</span>
</li>
<% end %>
</ul>
</ul>

View File

@ -16,6 +16,7 @@
<li><%= link_to 'Your characters', characters_path %></li>
<li><%= link_to 'Your locations', locations_path %></li>
<li><%= link_to 'Your items', items_path %></li>
<li><%= link_to 'Your attributes', attribute_fields_path %></li>
<li class="divider"></li>
<li><%= link_to 'Your author profile', current_user if current_user %></li>
<li><%= link_to 'Account settings', edit_user_registration_path %></li>
@ -75,6 +76,11 @@
<i class="material-icons">beach_access</i>
</a>
</li>
<li>
<a href="<%= attribute_fields_url %>" class="tooltipped" data-position="bottom" data-delay="100" data-tooltip="Attributes">
<i class="material-icons">assignment</i>
</a>
</li>
<li>
<a class="dropdown-button tooltipped" href="#!" data-activates="desktop-user-dropdown" data-position="bottom" data-delay="100" data-tooltip="You">
<i class="material-icons">person</i>
@ -108,4 +114,4 @@
</ul>
</div>
</nav>
</div>
</div>

View File

@ -22,6 +22,7 @@
<li><%= link_to "Character stats", admin_characters_path %></li>
<li><%= link_to "Location stats", admin_locations_path %></li>
<li><%= link_to "Item stats", admin_items_path %></li>
<li><%= link_to "Attribute stats", admin_attributes_path %></li>
</ul>
<%= yield %>

View File

@ -0,0 +1,14 @@
:general:
:label: General
:icon: info
:attributes:
- :name: label
:label: Label
- :name: field_type
:label: Field Type
- :name: attribute_category_id
:label: Attribute Category
- :name: name
:label: Name
- :name: description
:label: Description

View File

@ -0,0 +1,103 @@
:overview:
:label: Overview
:icon: info
:attributes:
- :name: name
:label: Name
- :name: role
:label: Role
- :name: gender
:label: Gender
- :name: age
:label: Age
- :name: universe_id
:label: Universe
:looks:
:label: Looks
:icon: face
:attributes:
- :name: weight
:label: Weight
- :name: height
:label: Height
- :name: haircolor
:label: Hair Color
- :name: hairstyle
:label: Hair Style
- :name: facialhair
:label: Facial Hair
- :name: eyecolor
:label: Eye Color
- :name: race
:label: Race
- :name: skintone
:label: Skin Tone
- :name: bodytype
:label: Body Type
- :name: identmarks
:label: Identifying Marks
:social:
:label: Social
:icon: groups
:attributes:
- :name: best_friends
:label: Best Friends
- :name: archenemies
:label: Arch Enemies
- :name: religion
:label: Religion
- :name: politics
:label: Politics
- :name: prejudices
:label: Prejudices
- :name: occupation
:label: Occupation
:history:
:label: history
:icon: info
:attributes:
- :name: birthday
:label: Birthday
- :name: birthplaces
:label: Birthplaces
- :name: education
:label: Education
- :name: background
:label: Background
:faves:
:label: Favorites
:icon: star
:attributes:
- :name: fave_color
:label: Color
- :name: fave_food
:label: Food
- :name: fave_possession
:label: Possession
- :name: fave_weapon
:label: Weapon
- :name: fave_animal
:label: Animal
:family:
:label: Family
:icon: face
:attributes:
- :name: mothers
:label: Mothers
- :name: fathers
:label: Fathers
- :name: spouses
:label: Spouses
- :name: siblings
:label: Siblings
- :name: children
:label: Children
:notes:
:label: Notes
:icon: edit
:attributes:
- :name: notes
:label: Notes
- :name: private_notes
:label: Private Notes
:description: Private notes are <em>always</em> visible to only you, even if content is made public and shared.

View File

@ -0,0 +1,46 @@
:overview:
:label: overview
:icon: info
:attributes:
- :name: name
:label: Name
- :name: item_type
:label: Item Type
- :name: description
:label: Description
- :name: universe_id
:label: Universe
:looks:
:label: Looks
:icon: redeem
:attributes:
- :name: materials
:label: Materials
- :name: weight
:label: Weight
:history:
:label: History
:icon: book
:attributes:
- :name: original_owners
:label: Original Owners
- :name: current_owners
:label: Current Owners
- :name: makers
:label: Makers
- :name: year_made
:label: Year it was made
:abilities:
:label: Abilities
:icon: flash_on
:attributes:
- :name: magic
:label: Magic
:notes:
:label: Notes
:icon: edit
:attributes:
- :name: notes
:label: Notes
- :name: private_notes
:label: Private Notes

View File

@ -0,0 +1,62 @@
:overview:
:label: overview
:icon: info
:attributes:
- :name: name
:label: Name
- :name: type_of
:label: Type
- :name: description
:label: Description
- :name: universe_id
:label: Universe
:culture:
:label: Culture
:icon: face
:attributes:
- :name: leaders
:label: Leaders
- :name: population
:label: Population
- :name: language
:label: Language
- :name: currency
:label: Currency
- :name: motto
:label: Motto
:cities:
:label: Cities
:icon: business
:attributes:
- :name: capital_cities
:label: Capital Cities
- :name: largest_cities
:label: Largest Cities
- :name: notable_cities
:label: Notable Cities
:geography:
:label: Geography
:icon: map
:attributes:
- :name: area
:label: Area
- :name: crops
:label: Crops
- :name: located_at
:label: Located At
:history:
:label: History
:icon: book
:attributes:
- :name: established_year
:label: Established Year
- :name: notable_wars
:label: Notable Wars
:notes:
:label: Notes
:icon: edit
:attributes:
- :name: notes
:label: Notes
- :name: private_notes
:label: Private Notes

View File

@ -0,0 +1,28 @@
:overview:
:label: overview
:icon: info
:attributes:
- :name: name
:label: Name
- :name: description
:label: Description
:history:
:label: History
:icon: book
:attributes:
- :name: history
:label: History
:notes:
:label: Notes
:icon: edit
:attributes:
- :name: notes
:label: Notes
- :name: private_notes
:label: Private Notes
:settings:
:label: Settings
:icon: build
:attributes:
- :name: privacy
:label: Privacy

View File

@ -143,6 +143,9 @@ en:
item: >
Books and weapons and artifacts, oh my! Track the items in your world: who made them, who owns them,
and what they can do.
attributefield: >
Attributes can be anything you need to define just the detail you need. Select the type of content
you're describing and setup any number of attributes to do the job.
create_success: "%{model_name} was successfully created."
update_success: "%{model_name} was successfully updated."

View File

@ -25,6 +25,10 @@ Rails.application.routes.draw do
# Planning
scope '/plan' do
resources :attributes
resources :attribute_categories
resources :attribute_fields
# Characters
resources :characters do
get :autocomplete_character_name, on: :collection, as: :autocomplete_name
@ -33,6 +37,7 @@ Rails.application.routes.draw do
resources :locations do
get :autocomplete_location_name, on: :collection, as: :autocomplete_name
end
resources :universes
# Coming Soon TM
@ -45,6 +50,7 @@ Rails.application.routes.draw do
scope 'admin' do
get '/', to: 'admin#dashboard', as: :admin_dashboard
get '/attributes', to: 'admin#attributes', as: :admin_attributes
get '/universes', to: 'admin#universes', as: :admin_universes
get '/characters', to: 'admin#characters', as: :admin_characters
get '/locations', to: 'admin#locations', as: :admin_locations

View File

@ -0,0 +1,45 @@
class CreateAttributes < ActiveRecord::Migration
def change
create_table :attribute_categories do |t|
t.belongs_to :user
t.string :entity_type
t.string :name, null: false
t.string :label, null: false
t.string :icon
t.text :description
t.timestamps
end
add_index :attribute_categories, :entity_type
add_index :attribute_categories, :name
create_table :attribute_fields do |t|
t.belongs_to :user
t.integer :attribute_category_id, null: false
t.string :name, null: false
t.string :label, null: false
t.string :field_type, null: false
t.text :description
t.string :privacy, default: 'private', null: false
t.timestamps
end
add_index :attribute_fields, [:user_id, :name]
create_table :attributes do |t|
t.belongs_to :user
t.integer :attribute_field_id
# Polymorphic association to owning entity
t.string :entity_type, null: false
t.integer :entity_id, null: false
# Attribute values
t.text :value
t.string :privacy, default: 'private', null: false
t.timestamps
end
add_index :attributes, [:user_id, :entity_type, :entity_id]
end
end

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160922204317) do
ActiveRecord::Schema.define(version: 20161003183741) do
create_table "archenemyships", force: :cascade do |t|
t.integer "user_id"
@ -21,6 +21,49 @@ ActiveRecord::Schema.define(version: 20160922204317) do
t.datetime "updated_at", null: false
end
create_table "attribute_categories", force: :cascade do |t|
t.integer "user_id"
t.string "entity_type"
t.string "name", null: false
t.string "label", null: false
t.string "icon"
t.text "description"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "attribute_categories", ["entity_type"], name: "index_attribute_categories_on_entity_type"
add_index "attribute_categories", ["name"], name: "index_attribute_categories_on_name"
create_table "attribute_fields", force: :cascade do |t|
t.integer "user_id"
t.integer "universe_id"
t.integer "attribute_category_id", null: false
t.string "name", null: false
t.string "label", null: false
t.string "field_type", null: false
t.text "description"
t.string "privacy", default: "private", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "attribute_fields", ["user_id", "name"], name: "index_attribute_fields_on_user_id_and_name"
create_table "attributes", force: :cascade do |t|
t.integer "user_id"
t.integer "universe_id"
t.integer "attribute_field_id"
t.string "entity_type", null: false
t.integer "entity_id", null: false
t.text "value"
t.string "privacy", default: "private", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "attributes", ["user_id", "entity_type", "entity_id"], name: "index_attributes_on_user_id_and_entity_type_and_entity_id"
create_table "best_friendships", force: :cascade do |t|
t.integer "user_id"
t.integer "character_id"

View File

@ -24,3 +24,21 @@ Item.create(name: 'Sting',
Location.create(name: 'The Shire',
user: tolkien,
universe: middleearth)
affiliation = AttributeCategory.create(name: 'affiliation',
entity_type: 'character',
user: tolkien,
label: 'Affiliation',
icon: 'verified_user')
affiliation.attribute_fields.create(name: 'starting_affiliation',
user: tolkien,
universe: middleearth,
label: 'Starting Affiliation',
field_type: 'text')
affiliation.attribute_fields.create(name: 'ending_affiliation',
user: tolkien,
universe: middleearth,
label: 'Ending Affiliation',
field_type: 'text')

View File

@ -0,0 +1,9 @@
require 'rails_helper'
require 'support/privacy_example'
require 'support/public_scope_example'
RSpec.describe Attribute, type: :model do
it_behaves_like 'content with privacy'
it_behaves_like 'content with an is_public scope'
it { is_expected.to validate_presence_of(:name) }
end