diff --git a/app/controllers/attribute_categories_controller.rb b/app/controllers/attribute_categories_controller.rb
new file mode 100644
index 00000000..c08cf154
--- /dev/null
+++ b/app/controllers/attribute_categories_controller.rb
@@ -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
diff --git a/app/controllers/attribute_fields_controller.rb b/app/controllers/attribute_fields_controller.rb
new file mode 100644
index 00000000..30a651da
--- /dev/null
+++ b/app/controllers/attribute_fields_controller.rb
@@ -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
diff --git a/app/controllers/characters_controller.rb b/app/controllers/characters_controller.rb
index fd498e98..fbdd8130 100644
--- a/app/controllers/characters_controller.rb
+++ b/app/controllers/characters_controller.rb
@@ -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],
diff --git a/app/controllers/concerns/has_ownership.rb b/app/controllers/concerns/has_ownership.rb
index 4d53f453..855e2b4a 100644
--- a/app/controllers/concerns/has_ownership.rb
+++ b/app/controllers/concerns/has_ownership.rb
@@ -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
diff --git a/app/controllers/content_controller.rb b/app/controllers/content_controller.rb
index 166b107e..c6d7012b 100644
--- a/app/controllers/content_controller.rb
+++ b/app/controllers/content_controller.rb
@@ -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
diff --git a/app/models/attribute.rb b/app/models/attribute.rb
new file mode 100644
index 00000000..f5df3197
--- /dev/null
+++ b/app/models/attribute.rb
@@ -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
diff --git a/app/models/attribute_category.rb b/app/models/attribute_category.rb
new file mode 100644
index 00000000..ceaa7af1
--- /dev/null
+++ b/app/models/attribute_category.rb
@@ -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
diff --git a/app/models/attribute_field.rb b/app/models/attribute_field.rb
new file mode 100644
index 00000000..0b159ca1
--- /dev/null
+++ b/app/models/attribute_field.rb
@@ -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
diff --git a/app/models/character.rb b/app/models/character.rb
index 7d5a28dc..7664fca8 100644
--- a/app/models/character.rb
+++ b/app/models/character.rb
@@ -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
diff --git a/app/models/concerns/has_attributes.rb b/app/models/concerns/has_attributes.rb
new file mode 100644
index 00000000..f4146e67
--- /dev/null
+++ b/app/models/concerns/has_attributes.rb
@@ -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
diff --git a/app/models/item.rb b/app/models/item.rb
index 280a2a51..6ce9d98a 100644
--- a/app/models/item.rb
+++ b/app/models/item.rb
@@ -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
diff --git a/app/models/location.rb b/app/models/location.rb
index 4974185e..d0d16e17 100644
--- a/app/models/location.rb
+++ b/app/models/location.rb
@@ -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
diff --git a/app/models/universe.rb b/app/models/universe.rb
index b8330521..af98c908 100644
--- a/app/models/universe.rb
+++ b/app/models/universe.rb
@@ -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
diff --git a/app/models/user.rb b/app/models/user.rb
index f4278db2..34cffe0f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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,
diff --git a/app/views/admin/attributes.html.erb b/app/views/admin/attributes.html.erb
new file mode 100644
index 00000000..edeefc80
--- /dev/null
+++ b/app/views/admin/attributes.html.erb
@@ -0,0 +1,20 @@
+
+
+
+
+
Characters per user
+ <%= 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 } %>
+
+
+
Attributes per universe
+ <%= 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 } %>
+
+
+
Attribute privacy
+ <%= pie_chart AttributeField.where.not(privacy: "").group(:privacy).count() %>
+
+
diff --git a/app/views/attribute_categories/_list.html.erb b/app/views/attribute_categories/_list.html.erb
new file mode 100644
index 00000000..652ecb8f
--- /dev/null
+++ b/app/views/attribute_categories/_list.html.erb
@@ -0,0 +1,31 @@
+
+
+
+ | Name |
+ Entity |
+ Description |
+ <% if session[:user] %>
+ <%=t '.actions', :default => t("helpers.actions") %> |
+ <% end %>
+
+
+
+
+ <% @content.each do |category| %>
+
+ | <%= simple_format link_to category.label, attribute_category_path(category) %> |
+ <%= simple_format category.entity_type.capitalize %> |
+ <%= simple_format category.description %> |
+
+ <% 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 %>
+ |
+
+ <% end %>
+
+
+
diff --git a/app/views/attribute_categories/edit.html.erb b/app/views/attribute_categories/edit.html.erb
new file mode 100644
index 00000000..4416094d
--- /dev/null
+++ b/app/views/attribute_categories/edit.html.erb
@@ -0,0 +1,3 @@
+<%= form_for @content do |form| %>
+ <%= render partial: 'content/form', locals: { f: form, content: @content } %>
+<% end %>
diff --git a/app/views/attribute_categories/new.html.erb b/app/views/attribute_categories/new.html.erb
new file mode 100644
index 00000000..4416094d
--- /dev/null
+++ b/app/views/attribute_categories/new.html.erb
@@ -0,0 +1,3 @@
+<%= form_for @content do |form| %>
+ <%= render partial: 'content/form', locals: { f: form, content: @content } %>
+<% end %>
diff --git a/app/views/attribute_categories/show.html.erb b/app/views/attribute_categories/show.html.erb
new file mode 100644
index 00000000..4c296455
--- /dev/null
+++ b/app/views/attribute_categories/show.html.erb
@@ -0,0 +1,12 @@
+
+
+<%= render partial: 'content/show', locals: { content: @content } %>
diff --git a/app/views/attribute_fields/_list.html.erb b/app/views/attribute_fields/_list.html.erb
new file mode 100644
index 00000000..4b40ed8c
--- /dev/null
+++ b/app/views/attribute_fields/_list.html.erb
@@ -0,0 +1,31 @@
+
+
+
+ | Name |
+ Type |
+ Description |
+ <% if session[:user] %>
+ <%=t '.actions', :default => t("helpers.actions") %> |
+ <% end %>
+
+
+
+
+ <% @content.each do |field| %>
+
+ | <%= simple_format link_to field.label, attribute_field_path(field) %> |
+ <%= simple_format field.field_type %> |
+ <%= simple_format field.description %> |
+
+ <% 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 %>
+ |
+
+ <% end %>
+
+
+
diff --git a/app/views/attribute_fields/edit.html.erb b/app/views/attribute_fields/edit.html.erb
new file mode 100644
index 00000000..4416094d
--- /dev/null
+++ b/app/views/attribute_fields/edit.html.erb
@@ -0,0 +1,3 @@
+<%= form_for @content do |form| %>
+ <%= render partial: 'content/form', locals: { f: form, content: @content } %>
+<% end %>
diff --git a/app/views/attribute_fields/new.html.erb b/app/views/attribute_fields/new.html.erb
new file mode 100644
index 00000000..4416094d
--- /dev/null
+++ b/app/views/attribute_fields/new.html.erb
@@ -0,0 +1,3 @@
+<%= form_for @content do |form| %>
+ <%= render partial: 'content/form', locals: { f: form, content: @content } %>
+<% end %>
diff --git a/app/views/attribute_fields/show.html.erb b/app/views/attribute_fields/show.html.erb
new file mode 100644
index 00000000..ab648149
--- /dev/null
+++ b/app/views/attribute_fields/show.html.erb
@@ -0,0 +1,12 @@
+
+
+<%= render partial: 'content/show', locals: { content: @content } %>
diff --git a/app/views/content/_form.html.erb b/app/views/content/_form.html.erb
index 4839d943..f4ed3fc3 100644
--- a/app/views/content/_form.html.erb
+++ b/app/views/content/_form.html.erb
@@ -15,129 +15,36 @@
<%# Tabs %>
+
<%# Content panels %>
<% content.class.attribute_categories.each do |category, data| %>
-
- <% 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
- %>
-
-
">
-
- <% 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' %>
-
- <%= f.label attribute %>
- <%= 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]) %>
-
-
- <% elsif attribute == 'privacy' %>
-
-
- <% elsif attribute == 'private_notes' %>
-
- <%= f.label attribute, attribute.humanize.capitalize %>
- <%= f.text_area attribute, class: "materialize-textarea", placeholder: 'Write as little or as much as you want!' %>
-
- Private notes are always visible to only you, even if content is made public and shared.
-
-
-
- <% 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 %>
-
- <% end %>
-
+ <%= render 'content/form/panel', category: category, f: f, content: content %>
<% end %>
+
- <%= 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}" %>
diff --git a/app/views/content/_show.html.erb b/app/views/content/_show.html.erb
index ca3bda5d..fd2380cb 100644
--- a/app/views/content/_show.html.erb
+++ b/app/views/content/_show.html.erb
@@ -1,6 +1,6 @@
<% if @content.present? && @content.respond_to?(:as_jsonld) %>
<% 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 %>
- <% content.class.attribute_categories.each do |category, data| %>
-
- <% data[:attributes].each do |attribute| %>
- <% next if attribute.start_with?("private") && @content.user != current_user %>
- <% next if content.send(attribute).blank? %>
-
-
<%= attribute.humanize.capitalize %>
+ <% content.class.attribute_categories.each do |category| %>
+
+ <% 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) %>
+
+
<%= attribute.label %>
<% if value.is_a?(ActiveRecord::Associations::CollectionProxy) %>
<% klass = value.first.class || value.build.class %>
@@ -49,7 +47,7 @@ set_meta_tags title: content.name, description: content.description
<% end %>
- <% elsif attribute == 'universe_id' %>
+ <% elsif attribute.name == 'universe_id' %>
<%= link_to content.universe.name, content.universe if content.universe %>
diff --git a/app/views/content/form/_panel.html.erb b/app/views/content/form/_panel.html.erb
new file mode 100644
index 00000000..bd356691
--- /dev/null
+++ b/app/views/content/form/_panel.html.erb
@@ -0,0 +1,107 @@
+
+ <% 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
+ %>
+
+
">
+
+ <% 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' %>
+
+ <%= f.label field.name, field.label %>
+ <%= 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]) %>
+
+
+ <% elsif field.name == 'attribute_category_id' %>
+
+ <%= f.label field.name, field.label %>
+ <%= 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) %>
+
+
+ <% elsif field.name == 'field_type' %>
+
+ <%= f.label field.name, field.label %>
+ <%= f.select field.name, ['Text Field'].map { |tf| [tf, tf.underscore] } %>
+
+
+ <% elsif field.name == 'privacy' %>
+
+
+ <% elsif field.name == 'private_notes' %>
+
+ <%= f.label field.name, field.label %>
+ <%= f.text_area field.name, class: "materialize-textarea", placeholder: 'Write as little or as much as you want!' %>
+
+ Private notes are always visible to only you, even if content is made public and shared.
+
+
+
+ <% 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 %>
+
+ <% end %>
+
diff --git a/app/views/content/form/_text_input.html.erb b/app/views/content/form/_text_input.html.erb
index 232dab7e..4097c35b 100644
--- a/app/views/content/form/_text_input.html.erb
+++ b/app/views/content/form/_text_input.html.erb
@@ -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.
%>
- <%= 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 %>
<% if defined?(autocomplete) && false %>
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/app/views/content/form/_title.html.erb b/app/views/content/form/_title.html.erb
index 767315f9..222742f3 100644
--- a/app/views/content/form/_title.html.erb
+++ b/app/views/content/form/_title.html.erb
@@ -1,5 +1,6 @@
<% if content.persisted? %>
<%= content.name %>
<% else %>
- Create your new <%= content.class.to_s.downcase %>
-<% end %>
\ No newline at end of file
+ <%- content_name = content.class.respond_to?(:content_name) ? content.class.content_name: content.class.to_s %>
+ Create your new <%= content_name.downcase %>
+<% end %>
diff --git a/app/views/content/index.html.erb b/app/views/content/index.html.erb
index 20b7c4ae..b2fea0b4 100644
--- a/app/views/content/index.html.erb
+++ b/app/views/content/index.html.erb
@@ -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? %>
-
You've created <%= pluralize(@content.count, content_type) %>
+
You've created <%= pluralize(@content.count, @content.content_name) %>
<%= 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? %>
-
You haven't created any <%= content_type.pluralize %> yet!
+
You haven't created any <%= @content.content_name.pluralize %> yet!
<%= content_type_class.icon %>
- <%= t("content_descriptions.#{content_type}") %>
+ <%= t("content_descriptions.#{content_type_class.name.downcase}") %>
- <%= 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' %>
<% end %>
diff --git a/app/views/content/list/_list.html.erb b/app/views/content/list/_list.html.erb
index 19fe299b..7eb1095f 100644
--- a/app/views/content/list/_list.html.erb
+++ b/app/views/content/list/_list.html.erb
@@ -2,6 +2,7 @@
<% if title.present? %>
<% end %>
+
<% content_list.each do |content| %>
<%= content.class.icon %>
@@ -39,4 +40,4 @@
<% end %>
-
\ No newline at end of file
+
diff --git a/app/views/layouts/_navbar.html.erb b/app/views/layouts/_navbar.html.erb
index 5c0f5ff0..9d6f642b 100644
--- a/app/views/layouts/_navbar.html.erb
+++ b/app/views/layouts/_navbar.html.erb
@@ -16,6 +16,7 @@
<%= link_to 'Your characters', characters_path %>
<%= link_to 'Your locations', locations_path %>
<%= link_to 'Your items', items_path %>
+
<%= link_to 'Your attributes', attribute_fields_path %>
<%= link_to 'Your author profile', current_user if current_user %>
<%= link_to 'Account settings', edit_user_registration_path %>
@@ -75,6 +76,11 @@
beach_access
+
+
+ assignment
+
+
person
@@ -108,4 +114,4 @@
-
\ No newline at end of file
+
diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb
index 3bd82d7c..412ab506 100644
--- a/app/views/layouts/admin.html.erb
+++ b/app/views/layouts/admin.html.erb
@@ -22,6 +22,7 @@
<%= link_to "Character stats", admin_characters_path %>
<%= link_to "Location stats", admin_locations_path %>
<%= link_to "Item stats", admin_items_path %>
+ <%= link_to "Attribute stats", admin_attributes_path %>
<%= yield %>
diff --git a/config/attributes/attribute.yml b/config/attributes/attribute.yml
new file mode 100644
index 00000000..86aa9301
--- /dev/null
+++ b/config/attributes/attribute.yml
@@ -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
diff --git a/config/attributes/character.yml b/config/attributes/character.yml
new file mode 100644
index 00000000..709cf6a9
--- /dev/null
+++ b/config/attributes/character.yml
@@ -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 always visible to only you, even if content is made public and shared.
diff --git a/config/attributes/item.yml b/config/attributes/item.yml
new file mode 100644
index 00000000..c81ccbea
--- /dev/null
+++ b/config/attributes/item.yml
@@ -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
diff --git a/config/attributes/location.yml b/config/attributes/location.yml
new file mode 100644
index 00000000..6a3311d9
--- /dev/null
+++ b/config/attributes/location.yml
@@ -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
diff --git a/config/attributes/universe.yml b/config/attributes/universe.yml
new file mode 100644
index 00000000..5fd11dd5
--- /dev/null
+++ b/config/attributes/universe.yml
@@ -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
diff --git a/config/locales/en.yml b/config/locales/en.yml
index d64e73e5..c9058919 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -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."
diff --git a/config/routes.rb b/config/routes.rb
index d1923497..1b9a132b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
diff --git a/db/migrate/20161003183741_create_attributes.rb b/db/migrate/20161003183741_create_attributes.rb
new file mode 100644
index 00000000..736416b8
--- /dev/null
+++ b/db/migrate/20161003183741_create_attributes.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
index a7dc2543..bbe06026 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -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"
diff --git a/db/seeds.rb b/db/seeds.rb
index 0de09571..c6b95d94 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -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')
diff --git a/spec/models/attribute_spec.rb b/spec/models/attribute_spec.rb
new file mode 100644
index 00000000..ee69b0bf
--- /dev/null
+++ b/spec/models/attribute_spec.rb
@@ -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