From 5a3cfcb6d19400a9f04aa06468e844614a3933de Mon Sep 17 00:00:00 2001 From: Matthew Werner Date: Mon, 10 Oct 2016 14:47:51 -0700 Subject: [PATCH] Convert attribute_categories to system categories --- .../attribute_categories_controller.rb | 15 +++ .../attribute_fields_controller.rb | 17 +++ app/controllers/characters_controller.rb | 1 + app/controllers/concerns/has_ownership.rb | 2 +- app/controllers/content_controller.rb | 15 ++- app/models/attribute.rb | 8 ++ app/models/attribute_category.rb | 27 ++++ app/models/attribute_field.rb | 38 ++++++ app/models/character.rb | 41 ++---- app/models/concerns/has_attributes.rb | 27 ++++ app/models/item.rb | 26 +--- app/models/location.rb | 31 +---- app/models/universe.rb | 24 +--- app/models/user.rb | 3 + app/views/admin/attributes.html.erb | 20 +++ app/views/attribute_categories/_list.html.erb | 31 +++++ app/views/attribute_categories/edit.html.erb | 3 + app/views/attribute_categories/new.html.erb | 3 + app/views/attribute_categories/show.html.erb | 12 ++ app/views/attribute_fields/_list.html.erb | 31 +++++ app/views/attribute_fields/edit.html.erb | 3 + app/views/attribute_fields/new.html.erb | 3 + app/views/attribute_fields/show.html.erb | 12 ++ app/views/content/_form.html.erb | 117 ++---------------- app/views/content/_show.html.erb | 26 ++-- app/views/content/form/_panel.html.erb | 107 ++++++++++++++++ app/views/content/form/_text_input.html.erb | 16 ++- app/views/content/form/_title.html.erb | 5 +- app/views/content/index.html.erb | 11 +- app/views/content/list/_list.html.erb | 3 +- app/views/layouts/_navbar.html.erb | 8 +- app/views/layouts/admin.html.erb | 1 + config/attributes/attribute.yml | 14 +++ config/attributes/character.yml | 103 +++++++++++++++ config/attributes/item.yml | 46 +++++++ config/attributes/location.yml | 62 ++++++++++ config/attributes/universe.yml | 28 +++++ config/locales/en.yml | 3 + config/routes.rb | 6 + .../20161003183741_create_attributes.rb | 45 +++++++ db/schema.rb | 45 ++++++- db/seeds.rb | 18 +++ spec/models/attribute_spec.rb | 9 ++ 43 files changed, 820 insertions(+), 246 deletions(-) create mode 100644 app/controllers/attribute_categories_controller.rb create mode 100644 app/controllers/attribute_fields_controller.rb create mode 100644 app/models/attribute.rb create mode 100644 app/models/attribute_category.rb create mode 100644 app/models/attribute_field.rb create mode 100644 app/models/concerns/has_attributes.rb create mode 100644 app/views/admin/attributes.html.erb create mode 100644 app/views/attribute_categories/_list.html.erb create mode 100644 app/views/attribute_categories/edit.html.erb create mode 100644 app/views/attribute_categories/new.html.erb create mode 100644 app/views/attribute_categories/show.html.erb create mode 100644 app/views/attribute_fields/_list.html.erb create mode 100644 app/views/attribute_fields/edit.html.erb create mode 100644 app/views/attribute_fields/new.html.erb create mode 100644 app/views/attribute_fields/show.html.erb create mode 100644 app/views/content/form/_panel.html.erb create mode 100644 config/attributes/attribute.yml create mode 100644 config/attributes/character.yml create mode 100644 config/attributes/item.yml create mode 100644 config/attributes/location.yml create mode 100644 config/attributes/universe.yml create mode 100644 db/migrate/20161003183741_create_attributes.rb create mode 100644 spec/models/attribute_spec.rb 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 @@ +
+
+

Attributes

+
+
+ +
+
+

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 @@ + + + + + + + <% if session[:user] %> + + <% end %> + + + + + <% @content.each do |category| %> + + + + + + + <% end %> + +
NameEntityDescription<%=t '.actions', :default => t("helpers.actions") %>
<%= 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 %> +
+ 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 @@ + + + + + + + <% if session[:user] %> + + <% end %> + + + + + <% @content.each do |field| %> + + + + + + + <% end %> + +
NameTypeDescription<%=t '.actions', :default => t("helpers.actions") %>
<%= 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 %> +
+ 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' %> -
- <%= f.label attribute %>
- <%= f.select attribute, [['Only visible to you', 'private'], ['Visible to anyone with the URL', 'public']] %> -
- 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. -
-
- - <% 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' %> +
+ <%= f.label field.name, field.label %>
+ <%= f.select field.name, [['Only visible to you', 'private'], ['Visible to anyone with the URL', 'public']] %> +
+ 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. +
+
+ + <% 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? %>
  • <%= title %>

  • <% 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