Merge branch 'master' into omniauth_for_social_sites

This commit is contained in:
Andrew Brown 2017-11-24 16:41:15 +01:00
commit edf2f4786a
1079 changed files with 3563 additions and 383 deletions

14
Gemfile
View File

@ -1,4 +1,5 @@
source 'https://rubygems.org'
ruby "~> 2.3.0"
gem 'rails', '4.2.7.1'
gem 'puma', '~> 3.6.0'
@ -58,7 +59,7 @@ gem 'mixpanel-ruby'
gem 'social-share-button'
# Apps
gem 'easy_translate'
#gem 'easy_translate'
gem 'levenshtein-ffi'
# Social Logins
@ -66,9 +67,19 @@ gem 'omniauth-facebook'
gem 'omniauth-twitter'
gem 'omniauth-google-oauth2'
# Forum
gem 'thredded', '~> 0.13.2'
gem 'rails-ujs'
gem 'delayed_job_active_record'
# Tech debt & hacks
gem 'binding_of_caller' # see has_changelog.rb
group :production do
gem 'rails_12factor'
gem 'uglifier', '>= 1.3.0'
gem 'newrelic_rpm'
end
group :test, :production do
@ -78,7 +89,6 @@ end
group :test do
gem 'better_errors'
gem 'binding_of_caller'
gem 'capybara'
gem 'codeclimate-test-reporter', require: false
gem 'coveralls', require: false

View File

@ -26,6 +26,8 @@ GEM
erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
active_record_union (1.2.0)
activerecord (>= 4.0)
activejob (4.2.7.1)
activesupport (= 4.2.7.1)
globalid (>= 0.3.0)
@ -47,11 +49,17 @@ GEM
ast (2.3.0)
authority (3.2.2)
activesupport (>= 3.0.0)
autoprefixer-rails (7.1.2.3)
execjs
aws-sdk (1.66.0)
aws-sdk-v1 (= 1.66.0)
aws-sdk-v1 (1.66.0)
json (~> 1.4)
nokogiri (>= 1.4.4)
babel-source (5.8.35)
babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6)
execjs (~> 2.0)
bcrypt (3.1.11)
better_errors (2.1.1)
coderay (>= 1.0.0)
@ -94,6 +102,7 @@ GEM
tins (>= 1.6.0, < 2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.2)
cucumber (2.4.0)
builder (>= 2.1.2)
cucumber-core (~> 1.5.0)
@ -114,7 +123,14 @@ GEM
database_cleaner (1.5.3)
dateslices (0.0.4)
rails (> 4)
db_text_search (0.2.2)
activerecord (>= 4.1.15, < 6.0)
debug_inspector (0.0.2)
delayed_job (4.1.3)
activesupport (>= 3.0, < 5.2)
delayed_job_active_record (4.1.2)
activerecord (>= 3.0, < 5.2)
delayed_job (>= 3.0, < 5)
devise (4.2.0)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
@ -125,10 +141,6 @@ GEM
docile (1.1.5)
domain_name (0.5.20161021)
unf (>= 0.0.5, < 1.0.0)
easy_translate (0.5.0)
json
thread
thread_safe
erubis (2.7.0)
execjs (2.7.0)
factory_girl (4.7.0)
@ -138,9 +150,13 @@ GEM
railties (>= 3.0.0)
faraday (0.12.2)
multipart-post (>= 1.2, < 3)
fast_blank (1.0.0)
ffi (1.9.14)
filesize (0.1.1)
formatador (0.2.5)
friendly_id (5.2.1)
activerecord (>= 4.0.0)
gemoji (2.1.0)
gherkin (4.0.0)
globalid (0.3.7)
activesupport (>= 4.1.0)
@ -162,14 +178,34 @@ GEM
rubocop (~> 0.20)
hashdiff (0.3.2)
hashie (3.5.6)
html-pipeline (2.6.0)
activesupport (>= 2)
nokogiri (>= 1.4)
htmlentities (4.3.4)
http-cookie (1.0.3)
domain_name (~> 0.5)
httparty (0.13.7)
json (~> 1.8)
multi_xml (>= 0.5.2)
i18n (0.8.0)
inline_svg (1.2.2)
activesupport (>= 3.0)
nokogiri (>= 1.6)
json (1.8.6)
jwt (1.5.6)
kaminari (1.0.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.0.1)
kaminari-activerecord (= 1.0.1)
kaminari-core (= 1.0.1)
kaminari-actionview (1.0.1)
actionview
kaminari-core (= 1.0.1)
kaminari-activerecord (1.0.1)
activerecord
kaminari-core (= 1.0.1)
kaminari-core (1.0.1)
kramdown (1.14.0)
levenshtein-ffi (1.1.0)
ffi (~> 1.9)
libv8 (3.16.14.15)
@ -195,14 +231,19 @@ GEM
mini_portile2 (2.1.0)
minitest (5.10.1)
mixpanel-ruby (2.2.0)
moneta (1.0.0)
multi_json (1.12.1)
multi_test (0.1.2)
multi_xml (0.6.0)
multipart-post (2.0.0)
mustache (1.0.5)
nenv (0.3.0)
netrc (0.11.0)
newrelic_rpm (4.5.0.337)
nokogiri (1.7.0.1)
mini_portile2 (~> 2.1.0)
nokogumbo (1.4.13)
nokogiri
notiffany (0.1.1)
nenv (~> 0.1)
shellany (~> 0.0)
@ -232,14 +273,23 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.8.16)
fast_blank (>= 1.0.0)
htmlentities (~> 4.3)
moneta (~> 1.0)
multi_json (~> 1.11)
mustache
nokogiri (~> 1.7)
sanitize
orm_adapter (0.5.0)
paperclip (4.2.4)
activemodel (>= 3.2.0)
activesupport (>= 3.2.0)
cocaine (~> 0.5.5)
mime-types
parser (2.3.1.4)
ast (~> 2.2)
parallel (1.12.0)
parser (2.4.0.2)
ast (~> 2.3)
pg (0.19.0)
powerpack (0.1.1)
pry (0.10.4)
@ -249,6 +299,8 @@ GEM
puma (3.6.0)
puma-heroku (1.0.0)
puma (~> 3.0)
pundit (1.1.0)
activesupport (>= 3.0.0)
rack (1.6.5)
rack-test (0.6.3)
rack (>= 1.0)
@ -275,6 +327,8 @@ GEM
rails-jquery-autocomplete (1.0.3)
rails (>= 3.2)
rails-perftest (0.0.6)
rails-ujs (0.1.0)
railties (>= 3.1)
rails_12factor (0.0.3)
rails_serve_static_assets
rails_stdout_logging
@ -285,13 +339,15 @@ GEM
activesupport (= 4.2.7.1)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.1.0)
rake (12.0.0)
rainbow (2.2.2)
rake
rake (12.1.0)
raygun4ruby (1.1.11)
httparty (~> 0.13.7)
json
rack
rb-fsevent (0.9.7)
rb-gravatar (1.0.5)
rb-inotify (0.9.7)
ffi (>= 0.5.0)
redcarpet (3.3.4)
@ -302,6 +358,7 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rinku (2.0.2)
rmagick (2.13.4)
rspec (3.5.0)
rspec-core (~> 3.5.0)
@ -327,17 +384,22 @@ GEM
rspec-mocks (~> 3.5.0)
rspec-support (~> 3.5.0)
rspec-support (3.5.0)
rubocop (0.43.0)
parser (>= 2.3.1.1, < 3.0)
rubocop (0.51.0)
parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
rainbow (>= 2.2.2, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-prof (0.15.9)
ruby-progressbar (1.8.1)
ruby-progressbar (1.9.0)
ruby_dep (1.4.0)
rubyzip (1.2.0)
rubyzip (1.2.1)
safe_yaml (1.0.4)
sanitize (4.5.0)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1)
sass (3.4.22)
sass-rails (5.0.6)
railties (>= 4.0.0, < 6)
@ -364,6 +426,10 @@ GEM
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-es6 (0.9.2)
babel-source (>= 5.8.11)
babel-transpiler
sprockets (>= 3.0.0)
sprockets-rails (3.2.0)
actionpack (>= 4.0)
activesupport (>= 4.0)
@ -377,9 +443,30 @@ GEM
libv8 (~> 3.16.14.0)
ref
thor (0.19.4)
thread (0.2.2)
thread_safe (0.3.5)
thredded (0.13.2)
active_record_union (>= 1.2.0)
autoprefixer-rails
db_text_search (~> 0.2.0)
friendly_id
gemoji (~> 2.1.0)
html-pipeline
htmlentities
inline_svg
kaminari
kramdown
nokogiri
onebox (~> 1.8, >= 1.8.13)
pundit (>= 1.1.0)
rails (>= 4.2.0)
rb-gravatar
rinku
sanitize
sass (>= 3.4.21)
sprockets-es6
timeago_js
tilt (2.0.5)
timeago_js (3.0.2)
tins (1.12.0)
tzinfo (1.2.2)
thread_safe (~> 0.1)
@ -390,7 +477,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
unicode-display_width (1.1.1)
unicode-display_width (1.3.0)
warden (1.2.6)
rack (>= 1.0)
webmock (2.3.2)
@ -419,8 +506,8 @@ DEPENDENCIES
cucumber-rails
database_cleaner
dateslices
delayed_job_active_record
devise
easy_translate
factory_girl_rails
filesize
guard
@ -431,6 +518,7 @@ DEPENDENCIES
medium-editor-rails
meta-tags
mixpanel-ruby
newrelic_rpm
omniauth-facebook
omniauth-google-oauth2
omniauth-twitter
@ -443,6 +531,7 @@ DEPENDENCIES
rails (= 4.2.7.1)
rails-jquery-autocomplete
rails-perftest
rails-ujs
rails_12factor
raygun4ruby
redcarpet
@ -461,9 +550,13 @@ DEPENDENCIES
sqlite3
stripe
therubyracer
thredded (~> 0.13.2)
tzinfo-data
uglifier (>= 1.3.0)
webmock
RUBY VERSION
ruby 2.3.1p112
BUNDLED WITH
1.15.3
1.16.0.pre.3

View File

@ -1,12 +1,12 @@
= notebook
= Notebook.ai
{<img src="https://travis-ci.org/indentlabs/notebook.png?branch=master" alt="Build Status" />}[https://travis-ci.org/indentlabs/notebook]
{<img src="https://codeclimate.com/github/indentlabs/notebook/badges/gpa.svg" />}[https://codeclimate.com/github/indentlabs/notebook]
{<img src="https://codeclimate.com/github/indentlabs/notebook/badges/coverage.svg" />}[https://codeclimate.com/github/indentlabs/notebook/coverage]
{<img src="http://inch-ci.org/github/indentlabs/notebook.svg?branch=master" alt="Inline docs" />}[http://inch-ci.org/github/indentlabs/notebook]
== What is notebook?
== What is Notebook.ai?
see {live website}[http://notebook.ai/]
notebook is a set of tools for writers, game designers, and roleplayers to create magnificent universes and everything within them.
From a simple interface in your browser, on your phone, or on your tablet, you can do everything you'd ever want to do while creating your own little (or big!) world.
@ -38,10 +38,9 @@ Install curl
sudo apt-get install curl
Install rvm
Install rvm (or your favorite ruby version manager)
\curl -sSL https://get.rvm.io | bash
source ~/.rvm/scripts/rvm
Install ruby 2.3.1
@ -50,13 +49,14 @@ Install ruby 2.3.1
rvm use 2.3.1
Install rails 4.2.5
Install rails 4.2.7
gem install rails -v 4.2.5
gem install rails -v 4.2.7
Install necessary libraries
sudo apt-get install imagemagick libmagickwand-dev
sudo apt-get install libpq-dev
Clone the code
@ -65,7 +65,7 @@ Clone the code
Install gems
bundle install
Create database
rake db:create
@ -73,16 +73,19 @@ Create database
Run initial database migrations
rake db:migrate
rake data_migrations:create_default_billing_plans
rake db:seed
Finally, run the server with
rails server
You should now see a copy of the site running locally at http://localhost:3000/!
== Running the notebook stack locally with Docker
- install {Docker}[https://www.docker.com/products/overview]
- install {Docker Compose}[https://docs.docker.com/compose/install]
- clone this git repo
- cd into the root of this repo, and then run
docker-compose up

View File

@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@ -1,3 +1,3 @@
// Place all the styles related to the Documents controller here.
// Place all the styles related to the Contributors controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@ -1,3 +0,0 @@
// Place all the styles related to the Emails controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@ -1,3 +0,0 @@
// Place all the styles related to the Lab controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@ -1,3 +0,0 @@
// Place all the styles related to the Navigator controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@ -1,3 +0,0 @@
// Place all the styles related to the Voting controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@ -0,0 +1,29 @@
class AttributeAuthorizer < ApplicationAuthorizer
def self.creatable_by? user
[
true
].any?
end
def readable_by? user
[
PermissionService.user_owns_content?(user: user, content: resource),
PermissionService.user_owns_any_containing_universe?(user: user, content: resource),
PermissionService.user_can_contribute_to_containing_universe?(user: user, content: resource),
PermissionService.content_is_public?(content: resource),
PermissionService.content_is_in_a_public_universe?(content: resource)
].any?
end
def updatable_by? user
[
PermissionService.user_owns_content?(user: user, content: resource),
].any?
end
def deletable_by? user
[
PermissionService.user_owns_content?(user: user, content: resource),
].any?
end
end

View File

@ -1,5 +1,10 @@
class CollectiveContentAuthorizer < ContentAuthorizer
def self.creatable_by? user
user.active_billing_plans.any? { |plan| plan.allows_collective_content } || user.selected_billing_plan_id == 2
return false if ENV.key?('CONTENT_BLACKLIST') && ENV['CONTENT_BLACKLIST'].split(',').include?(user.email)
[
PermissionService.billing_plan_allows_collective_content?(user: user),
PermissionService.user_can_collaborate_in_universe_that_allows_collective_content?(user: user)
].any?
end
end

View File

@ -1,19 +1,36 @@
class ContentAuthorizer < ApplicationAuthorizer
def readable_by? user
[
resource.user_id == user.id,
resource.respond_to?(:privacy) && resource.privacy == 'public',
resource.respond_to?(:universe) && resource.universe.present? && resource.universe.privacy == 'public'
PermissionService.user_owns_any_containing_universe?(user: user, content: resource),
PermissionService.user_owns_content?(user: user, content: resource),
PermissionService.content_is_public?(content: resource),
PermissionService.content_is_in_a_public_universe?(content: resource),
PermissionService.user_can_contribute_to_containing_universe?(user: user, content: resource)
].any?
end
def updatable_by? user
#todo: Collaboration
resource.user_id == user.id
[
PermissionService.user_owns_any_containing_universe?(user: user, content: resource),
PermissionService.user_can_contribute_to_containing_universe?(user: user, content: resource),
[
PermissionService.content_has_no_containing_universe?(content: resource),
PermissionService.user_owns_content?(user: user, content: resource)
].all?
].any?
end
def deletable_by? user
#todo: Collaboration
resource.user_id == user.id
[
PermissionService.user_owns_any_containing_universe?(user: user, content: resource),
[
PermissionService.content_has_no_containing_universe?(content: resource),
PermissionService.user_owns_content?(user: user, content: resource)
].all?,
[
PermissionService.user_can_contribute_to_containing_universe?(user: user, content: resource),
PermissionService.user_owns_content?(user: user, content: resource)
].all?
].any?
end
end

View File

@ -1,7 +1,9 @@
class CoreContentAuthorizer < ContentAuthorizer
def self.creatable_by? user
active_billing_plans = user.active_billing_plans
return false if ENV.key?('CONTENT_BLACKLIST') && ENV['CONTENT_BLACKLIST'].split(',').include?(user.email)
active_billing_plans.empty? || active_billing_plans.any? { |plan| plan.allows_core_content }
[
PermissionService.billing_plan_allows_core_content?(user: user)
].any?
end
end

View File

@ -1,5 +1,10 @@
class ExtendedContentAuthorizer < ContentAuthorizer
def self.creatable_by? user
user.active_billing_plans.any? { |plan| plan.allows_extended_content } || user.selected_billing_plan_id == 2
return false if ENV.key?('CONTENT_BLACKLIST') && ENV['CONTENT_BLACKLIST'].split(',').include?(user.email)
[
PermissionService.billing_plan_allows_extended_content?(user: user),
PermissionService.user_can_collaborate_in_universe_that_allows_extended_content?(user: user)
].any?
end
end

View File

@ -1,24 +1,31 @@
class UniverseCoreContentAuthorizer < CoreContentAuthorizer
def self.creatable_by? user
if BillingPlan.find_by(stripe_plan_id: 'free-for-life')
return true if user.selected_billing_plan_id == BillingPlan.find_by(stripe_plan_id: 'free-for-life').id
end
user.universes.count < (user.active_billing_plans.map(&:universe_limit).max || 5)
return false if ENV.key?('CONTENT_BLACKLIST') && ENV['CONTENT_BLACKLIST'].split(',').include?(user.email)
[
PermissionService.user_has_fewer_owned_universes_than_plan_limit?(user: user),
PermissionService.user_is_on_premium_plan?(user: user)
].any?
end
def readable_by? user
[
resource.user_id == user.id,
resource.privacy == 'public'
].any?
PermissionService.content_is_public?(content: resource),
PermissionService.user_owns_content?(user: user, content: resource),
PermissionService.user_can_contribute_to_universe?(user: user, universe: resource)
].any?
end
def updatable_by? user
resource.user_id == user.id
[
PermissionService.user_owns_content?(user: user, content: resource),
PermissionService.user_can_contribute_to_universe?(user: user, universe: resource)
].any?
end
def deletable_by? user
resource.user_id == user.id
[
PermissionService.user_owns_content?(user: user, content: resource)
].any?
end
end

View File

@ -6,7 +6,8 @@ class ApplicationController < ActionController::Base
if params[:universe] == 'all'
session.delete(:universe_id)
elsif params[:universe].is_a?(String) && params[:universe].to_i.to_s == params[:universe]
found_universe = Universe.find_by(user: current_user, id: params[:universe])
found_universe = Universe.find_by(id: params[:universe])
found_universe = nil unless current_user.universes.include?(found_universe) || current_user.contributable_universes.include?(found_universe)
session[:universe_id] = found_universe.id if found_universe
end
end
@ -14,7 +15,8 @@ class ApplicationController < ActionController::Base
before_action do
if current_user && session[:universe_id]
@universe_scope = Universe.find_by(user: current_user, id: session[:universe_id])
@universe_scope = Universe.find_by(id: session[:universe_id])
@universe_scope = nil unless current_user.universes.include?(@universe_scope) || current_user.contributable_universes.include?(@universe_scope)
else
@universe_scope = nil
end

View File

@ -19,7 +19,7 @@ class AttributeFieldsController < ContentController
Mixpanel::Tracker.new(Rails.application.config.mixpanel_token).track(current_user.id, 'created attribute', {
'content_type': params[:entity_type]
})
}) if Rails.env.production?
@content = AttributeField.new(label: content_params[:label]).tap do |f|
f.attribute_category_id = category.id

View File

@ -1,64 +0,0 @@
# Module handles privacy and auth for controller content
module HasOwnership
extend ActiveSupport::Concern
included do
before_action :require_ownership_or_sharing, only: [:show]
before_action :require_ownership, only: [:edit, :update, :destroy]
end
module ClassMethods
def owner # TODO: is this even used?
user.id
end
end
private
# Ensures that only the owner can do this action
def require_ownership
model = content_type_from_controller(self.class)
redirect_if_not_owned model.find(params[:id]), send(redirect_path)
# rescue
# redirect_to '/500' unless Rails.env.development?
end
# Unless this content is shared, ensure only the owner can do this action
def require_ownership_or_sharing
model = content_type_from_controller(self.class).find(params[:id])
unless model.user == current_user || publicly_shared?(model)
if current_user
redirect_to send(redirect_path), notice: t(:no_do_permission)
else
redirect_to new_user_session_path, notice: t(:no_do_permission)
end
end
end
# Redirect the user to redirect_path unless they own this content
def redirect_if_not_owned(object_to_check, redirect_path)
if current_user.nil?
redirect_to new_user_session_path, notice: t(:no_do_permission)
elsif current_user != object_to_check.user
redirect_to send(redirect_path), notice: t(:no_do_permission)
end
end
def redirect_path
model = content_type_from_controller(self.class)
# TODO: proper pluralizing here
"#{model.to_s.underscore}s_path"
end
def owned_by_current_user?(object)
current_user == object.user
end
def publicly_shared?(object)
object_is_public = object.respond_to?(:privacy) && object.privacy.present? && object.privacy.casecmp('public').zero?
object_universe_is_public = object.respond_to?(:universe) && object.universe.present? && object.universe.privacy.casecmp('public').zero?
# If either the content or the universe it's in are public, we treat the object as public
object_is_public || object_universe_is_public
end
end

View File

@ -1,15 +1,25 @@
class ContentController < ApplicationController
include HasOwnership
before_action :authenticate_user!, only: [:index, :new, :create, :edit, :update, :destroy]
def index
@content = content_type_from_controller(self.class)
.where(user_id: current_user.id)
.order(:name)
@content_type_class = content_type_from_controller(self.class)
pluralized_content_name = @content_type_class.name.downcase.pluralize
if @universe_scope.present? && @content_type_class != Universe
@content = @universe_scope.send(pluralized_content_name)
else
@content = (
current_user.send(pluralized_content_name) +
current_user.send("contributable_#{pluralized_content_name}")
)
unless @content_type_class == Universe
@content.concat(current_user.universes.flat_map { |universe| universe.send(pluralized_content_name) })
end
end
@content = @content.flatten.uniq.sort_by(&:name)
@content = @content.where(universe: @universe_scope) if @universe_scope.present? && @content.build.respond_to?(:universe)
@content ||= []
@questioned_content = @content.sample
@question = @questioned_content.question unless @questioned_content.nil?
@ -24,6 +34,8 @@ class ContentController < ApplicationController
# TODO: Secure this with content class whitelist lel
@content = content_type.find(params[:id])
return if ENV.key?('CONTENT_BLACKLIST') && ENV['CONTENT_BLACKLIST'].split(',').include?(@content.user.email)
if (current_user || User.new).can_read? @content
@question = @content.question if current_user.present? and current_user == @content.user
@ -49,9 +61,9 @@ class ContentController < ApplicationController
end
else
if current_user.present?
return redirect_to :back
return redirect_to :back, notice: "You don't have permission to view that content."
else
return redirect_to root_path
return redirect_to root_path, notice: "You don't have permission to view that content."
end
end
end
@ -75,7 +87,7 @@ class ContentController < ApplicationController
.find(params[:id])
unless @content.updatable_by? current_user
return redirect_to :back
return redirect_to @content, notice: t(:no_do_permission)
end
respond_to do |format|
@ -92,6 +104,15 @@ class ContentController < ApplicationController
return redirect_to :back
end
# Even if a user can create content, we want to double check that they're either on a premium account or creating content in a universe owned by someone on premium
# unless current_user.on_premium_plan?
# containing_universe = Universe.find(content_params[:universe_id].to_i)
# unless content_params[:universe_id].present? && containing_universe && containing_universe.user.on_premium_plan? && current_user.contributable_universes.include?(containing_universe)
# return redirect_to send(content_type.name.downcase.pluralize + '_path'),
# notice: "Premium content must either be created by a user with a Premium account, or in a universe owned by a user with a Premium account."
# end
# end
Mixpanel::Tracker.new(Rails.application.config.mixpanel_token).track(current_user.id, 'created content', {
'content_type': content_type.name
}) if Rails.env.production?
@ -123,7 +144,21 @@ class ContentController < ApplicationController
upload_files params['image_uploads'], content_type.name, @content.id
end
if @content.update_attributes(content_params)
if @content.is_a?(Universe) && params.key?('contributors') && @content.user == current_user
params[:contributors][:email].reject(&:blank?).each do |email|
ContributorService.invite_contributor_to_universe(universe: @content, email: email)
end
end
if @content.user == current_user
update_success = @content.update_attributes(content_params)
else
# Exclude fields only the real owner can edit
#todo move field list somewhere when it grows
update_success = @content.update_attributes(content_params.except!(:universe_id))
end
if update_success
successful_response(@content, t(:update_success, model_name: humanized_model_name))
else
failed_response('edit', :unprocessable_entity)
@ -165,7 +200,7 @@ class ContentController < ApplicationController
@content = content_type.find(params[:id])
unless current_user.can_delete? @content
return redirect_to :back
return redirect_to :back, notice: "You don't have permission to do that!"
end
Mixpanel::Tracker.new(Rails.application.config.mixpanel_token).track(current_user.id, 'deleted content', {

View File

@ -0,0 +1,16 @@
class ContributorsController < ApplicationController
def destroy
contributor = Contributor.find(params[:id])
relevant_universe = Universe.find(contributor.universe_id)
if contributor
contributor.destroy
#todo send "you've been removed as a contributor" email
end
# A 303 status is required here so the browser doesn't redirect with a DELETE action
# https://stackoverflow.com/questions/14598703/rails-redirect-after-delete-using-delete-instead-of-get
redirect_to universes_path, status: 303
end
end

View File

@ -46,6 +46,11 @@ class ExportController < ApplicationController
send_data to_csv(current_user.races), filename: "races-#{Date.today}.csv"
end
def floras_csv
report_to_mixpanel 'csv', 'flora'
send_data to_csv(current_user.floras), filename: "floras-#{Date.today}.csv"
end
def religions_csv
report_to_mixpanel 'csv', 'religions'
send_data to_csv(current_user.religions), filename: "religions-#{Date.today}.csv"

View File

@ -15,7 +15,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def account_update_params
params.require(:user).permit(:name, :email, :password, :password_confirmation, :email_updates, :fluid_preference)
params.require(:user).permit(:name, :email, :username, :password, :password_confirmation, :email_updates, :fluid_preference)
end
protected
@ -25,6 +25,11 @@ class RegistrationsController < Devise::RegistrationsController
end
def add_account
# Tie any universe contributor invites with this email to this user
if resource.persisted?
Contributor.where(email: resource.email, user_id: nil).update_all(user_id: resource.id)
end
# If the user was created in the last 60 seconds, report it to Slack
if resource.persisted?
report_new_account_to_slack resource

View File

@ -52,6 +52,12 @@ class SubscriptionsController < ApplicationController
end_date: Time.now.end_of_day + 5.years
)
unless Rails.env.test?
stripe_customer = Stripe::Customer.retrieve current_user.stripe_customer_id
stripe_subscription = stripe_customer.subscriptions.data[0]
stripe_subscription.save
end
report_subscription_change_to_slack current_user, old_billing_plan, new_billing_plan
flash[:notice] = "You have been successfully downgraded to Starter." #todo proration/credit
@ -72,14 +78,14 @@ class SubscriptionsController < ApplicationController
# Change subscription plan if they already have a payment method on file
stripe_subscription.plan = new_plan_id
begin
stripe_subscription.save
stripe_subscription.save unless Rails.env.test?
rescue Stripe::CardError => e
flash[:alert] = "We couldn't upgrade you to Premium because #{e.message.downcase} Please double check that your information is correct."
return redirect_to :back
end
# If this is the first time this user is subscribing to Premium, gift them (and their referrer, if applicable) feature votes and space
existing_premium_subscriptions = current_user.subscriptions.where(billing_plan_id: [2, 3, 4, 5, 6])
existing_premium_subscriptions = current_user.subscriptions.where(billing_plan_id: BillingPlan::PREMIUM_IDS)
unless existing_premium_subscriptions.any?
referring_user = current_user.referrer || current_user
@ -223,7 +229,7 @@ class SubscriptionsController < ApplicationController
end
# If this is the first time this user is subscribing to Premium, gift them (and their referrer, if applicable) feature votes and space
existing_premium_subscriptions = current_user.subscriptions.where(billing_plan_id: [2, 3, 4, 5, 6])
existing_premium_subscriptions = current_user.subscriptions.where(billing_plan_id: BillingPlan::PREMIUM_IDS)
unless existing_premium_subscriptions.any?
referring_user = current_user.referrer || current_user
@ -323,14 +329,20 @@ class SubscriptionsController < ApplicationController
delta = ":wave: Downgrade"
end
active_subscriptions = Subscription.where('start_date < ?', Time.now).where('end_date > ?', Time.now)
total_subs_monthly = active_subscriptions.map(&:billing_plan).sum(&:monthly_cents).to_f / 100.0
total_subscriptions = 0
monthly_rev_cents = 0
billing_plans_with_prices = BillingPlan.where.not(monthly_cents: 0).pluck(:id, :monthly_cents)
billing_plans_with_prices.each do |plan_id, monthly_cents|
users_on_this_plan = User.where(selected_billing_plan_id: plan_id).count
total_subscriptions += users_on_this_plan
monthly_rev_cents += monthly_cents * users_on_this_plan
end
notifier.ping [
"#{delta} for #{user.email.split('@').first}@ (##{user.id})",
"From: *#{from.name}* ($#{from.monthly_cents / 100.0}/month)",
"To: *#{to.name}* (#{to.stripe_plan_id}) ($#{to.monthly_cents / 100.0}/month)",
"#{active_subscriptions.count} subscriptions total $#{'%.2f' % total_subs_monthly}/mo"
"#{total_subscriptions} subscriptions total $#{'%.2f' % (monthly_rev_cents / 100)}/mo"
].join("\n")
end

View File

@ -28,7 +28,7 @@ class UniversesController < ContentController
:history,
:privacy,
:notes, :private_notes,
custom_attribute_values: [:name, :value]
custom_attribute_values: [:name, :value],
]
end
end

View File

@ -0,0 +1,2 @@
module ContributorsHelper
end

View File

@ -0,0 +1,4 @@
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
layout 'mailer'
end

View File

@ -0,0 +1,13 @@
class CollaborationMailer < ApplicationMailer
default from: "collaboration@notebook.ai"
def contributor_invitation(inviter:, invite_email:, universe:)
@inviter = inviter
@universe = universe
mail(
to: invite_email,
subject: "#{@inviter.name} requested your help collaborating on Notebook.ai."
)
end
end

View File

@ -4,7 +4,7 @@ class Attribute < ActiveRecord::Base
belongs_to :entity, polymorphic: true
include Authority::Abilities
self.authorizer_name = 'CoreContentAuthorizer'
self.authorizer_name = 'AttributeAuthorizer'
include HasPrivacy
scope :is_public, -> { eager_load(:universe).where('universes.privacy = ? OR attributes.privacy = ?', 'public', 'public') }

View File

@ -8,7 +8,7 @@ class AttributeCategory < ActiveRecord::Base
include Serendipitous::Concern
include Authority::Abilities
self.authorizer_name = 'CoreContentAuthorizer'
self.authorizer_name = 'AttributeAuthorizer'
before_validation :ensure_name

View File

@ -9,7 +9,7 @@ class AttributeField < ActiveRecord::Base
include Serendipitous::Concern
include Authority::Abilities
self.authorizer_name = 'CoreContentAuthorizer'
self.authorizer_name = 'AttributeAuthorizer'
attr_accessor :system

View File

@ -1,2 +1,3 @@
class BillingPlan < ActiveRecord::Base
PREMIUM_IDS = [2, 3, 4, 5, 6]
end

View File

@ -10,7 +10,7 @@ module HasChangelog
after_create do
ContentChangeEvent.create(
user: user,
user: find_current_user,
changed_fields: changes,
content_id: id,
content_type: self.class.name,
@ -19,8 +19,9 @@ module HasChangelog
end
after_update do
# todo how to get current_user?
ContentChangeEvent.create(
user: user,
user: find_current_user,
changed_fields: changes,
content_id: id,
content_type: self.class.name,
@ -30,12 +31,24 @@ module HasChangelog
before_destroy do
ContentChangeEvent.create(
user: user,
user: find_current_user,
changed_fields: changes,
content_id: id,
content_type: self.class.name,
action: :deleted
)
end
private
def find_current_user
(1..Kernel.caller.length).each do |n|
RubyVM::DebugInspector.open do |i|
current_user = eval "current_user rescue nil", i.frame_binding(n)
return current_user unless current_user.nil?
end
end
return nil
end
end
end

View File

@ -22,7 +22,7 @@ module HasContentGroupers
} # }
# Siblingships
has_many connecting_class_name
has_many connecting_class_name, dependent: :destroy
# Siblings
has_many relation,

View File

@ -39,6 +39,13 @@ class Childrenship < ActiveRecord::Base
elsif gender == :female
other_object.mothers.delete this_object
elsif gender.nil?
if other_object.fathers.include? this_object
other_object.fathers.delete this_object
elsif other_object.mothers.include? this_object
other_object.mothers.delete this_object
end
end
end

View File

@ -16,6 +16,6 @@ class Siblingship < ActiveRecord::Base
this_object = Character.find_by(id: self.character_id)
other_object = Character.find_by(id: self.sibling_id)
other_object.siblings.delete this_object
other_object.siblings.delete(this_object) unless other_object.nil?
end
end

View File

@ -18,6 +18,7 @@ class Universe < ActiveRecord::Base
self.authorizer_name = 'UniverseCoreContentAuthorizer'
validates :name, presence: true
validates :user_id, presence: true
belongs_to :user
# Core content types
@ -36,6 +37,8 @@ class Universe < ActiveRecord::Base
has_many :scenes
has_many :groups
has_many :contributors, dependent: :destroy
scope :is_public, -> { where(privacy: 'public') }
def content

View File

@ -0,0 +1,4 @@
class Contributor < ActiveRecord::Base
belongs_to :universe
belongs_to :user
end

View File

@ -14,11 +14,15 @@ class User < ActiveRecord::Base
include HasContent
include Authority::UserAbilities
validates :email, presence: true
#todo: We probably want a uniqueness constraint on email
validates_uniqueness_of :username, allow_nil: true, allow_blank: true
has_many :subscriptions
has_many :billing_plans, through: :subscriptions
def on_premium_plan?
return true
BillingPlan::PREMIUM_IDS.include?(self.selected_billing_plan_id)
end
has_many :image_uploads
has_one :referral_code
@ -35,6 +39,22 @@ class User < ActiveRecord::Base
has_many :omniauth_users
def contributable_universes
# todo email confirmation needs to happy for data safety / privacy (only verified emails)
contributor_by_email = Contributor.where(email: self.email).pluck(:universe_id)
contributor_by_user = Contributor.where(user: self).pluck(:universe_id)
Universe.where(id: contributor_by_email + contributor_by_user)
end
[Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Scene, Flora].each do |content_type|
pluralized_content_type = content_type.name.downcase.pluralize
define_method "contributable_#{pluralized_content_type}" do
contributable_universes.flat_map do |universe|
universe.send(pluralized_content_type).where.not(user_id: self.id)
end
end
end
# TODO: Swap this out with a has_many when we transition from a scratchpad to users having multiple documents
has_one :document
@ -43,7 +63,7 @@ class User < ActiveRecord::Base
after_create :initialize_secure_code
def createable_content_types
[Universe, Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Scene].select do |c|
[Universe, Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Scene, Flora].select do |c|
can_create? c
end
end
@ -138,4 +158,12 @@ class User < ActiveRecord::Base
:email
]
end
def forum_username
username = self.username.present? ? "@#{self.username}" : nil
username ||= self.name.present? ? self.name : nil
username ||= 'Anonymous Author'
username
end
end

View File

@ -0,0 +1,39 @@
class ContributorService < Service
def self.invite_contributor_to_universe universe:, email:
# First, look up whether a user already exists for this invite
related_user = User.find_by(email: email)
# Create the Contributor object with or without a user
Contributor.create(
universe: universe,
email: email,
user: related_user
)
# If the user doesn't already have a Notebook.ai account, send them an invite
if related_user.nil?
send_invite_email_to(inviter: universe.user, email: email, universe: universe)
end
# If the user does have a Notebook.ai account, send them an email letting them know they got added as a contributor
if related_user
send_contributor_notice_to(inviter: universe.user, email: email, universe: universe)
end
end
def self.send_invite_email_to inviter:, email:, universe:
CollaborationMailer.contributor_invitation(
inviter: inviter,
invite_email: email,
universe: universe
)
end
def self.send_contributor_notice_to inviter:, email:, universe:
CollaborationMailer.contributor_invitation(
inviter: inviter,
invite_email: email,
universe: universe
)
end
end

View File

@ -0,0 +1,66 @@
# A helper for more human-readable logic clauses to be used in permissions
class PermissionService < Service
def self.user_has_fewer_owned_universes_than_plan_limit?(user:)
user.universes.count < (user.active_billing_plans.map(&:universe_limit).max || 5)
end
def self.user_owns_content?(user:, content:)
content.user == user
end
def self.user_owns_any_containing_universe?(user:, content:)
content.respond_to?(:universe) && content.universe.present? && user_owns_content?(user: user, content: content.universe)
end
def self.user_can_contribute_to_universe?(user:, universe:)
user.contributable_universes.pluck(:id).include?(universe.id)
end
def self.content_is_public?(content:)
content.respond_to?(:privacy) && content.privacy == 'public'
end
def self.content_is_in_a_public_universe?(content:)
content.respond_to?(:universe) && content.universe.present? && self.content_is_public?(content: content.universe)
end
def self.user_can_contribute_to_containing_universe?(user:, content:)
content.universe.present? && user.contributable_universes.pluck(:id).include?(content.universe.id)
end
def self.content_has_no_containing_universe?(content:)
content.universe.nil?
end
def self.user_is_on_premium_plan?(user:)
user.on_premium_plan?
end
def self.billing_plan_allows_core_content?(user:)
active_billing_plans = user.active_billing_plans
active_billing_plans.empty? || active_billing_plans.any?(&:allows_core_content)
end
def self.billing_plan_allows_extended_content?(user:)
# todo remove second clause will billing plans are fixed
user.active_billing_plans.any?(&:allows_extended_content) || BillingPlan::PREMIUM_IDS.include?(user.selected_billing_plan_id)
end
def self.user_can_collaborate_in_universe_that_allows_extended_content?(user:)
user.contributable_universes.any? do |universe|
billing_plan_allows_extended_content?(user: universe.user)
end
end
def self.billing_plan_allows_collective_content?(user:)
user.active_billing_plans.any?(&:allows_collective_content) || BillingPlan::PREMIUM_IDS.include?(user.selected_billing_plan_id)
end
def self.user_can_collaborate_in_universe_that_allows_collective_content?(user:)
user.contributable_universes.any? do |universe|
billing_plan_allows_collective_content?(user: universe.user)
end
end
end

View File

@ -5,7 +5,7 @@
<div class="card hoverable" style="min-height: 330px;">
<div class="card-image">
<%= image_tag "card-headers/#{content_name.pluralize}.jpg" %>
<%= image_tag "card-headers/#{content_name.pluralize}.jpg", height: 300, width: 300 %>
<span class="card-title">Create <%= content_name == "magic" ? 'magic' : content_name.pluralize %></span>
</div>
<div class="card-content">
@ -13,4 +13,4 @@
<%= t("content_descriptions.#{content_name}") %>
</p>
</div>
</div>
</div>

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
</head>
<body>
<h1>Hello!</h1>
<p>
<%= @inviter.name %> is creating their own universes on Notebook.ai and has invited you to collaborate on their <%= link_to @universe.name, universe_path(@universe, referral_code: @universe.user.referral_code.code) %> universe.
</p>
<p>
To get started, simply sign up or sign in to <%= link_to 'Notebook.ai', "https://www.notebook.ai/users/sign_in?referral_code=#{@universe.user.referral_code.code}" %> with this email address and look for "<%= @universe.name %>" under your Universes tab.
</p>
<p>
Happy worldbuilding!
</p>
<br /><br /><br />
<p style="font-size: 80%">
Please don't respond to this email as this email address is not watched. If you have any questions, comments, concerns, or want to unsubscribe to future collaboration invitations, please send an email directly to andrew@indentlabs.com. Thank you for your understanding.
</p>
</body>
</html>

View File

@ -0,0 +1,13 @@
Hello!
<%= @inviter.name %> is creating their own universes on Notebook.ai and has invited you to collaborate on their <%= @universe.name %> universe.
To get started, simply sign up or sign in to https://www.notebook.ai/ with this email address and look for <%= @universe.name %> under your Universes tab. You can also navigate directly to the universe at https://www.notebook.ai<%= universe_path(@universe) %> and get started from there.
If you are signing up for an account for the first time, you can use the following link to credit <%= @universe.user.name %> with a referral: <%= "https://www.notebook.ai/?referral_code=#{@universe.user.referral_code.code}" %>
Happy worldbuilding!
-
Please don't respond to this email as the address is not watched. If you have any questions, comments, concerns, or want to unsubscribe to future collaboration invitations, please send an email directly to andrew@indentlabs.com. Thank you for your understanding.

View File

@ -1,4 +1,4 @@
<% content_to_show = [Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Scene] %>
<% content_to_show = [Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Scene, Flora] %>
<% content_to_show.each do |content_type| %>
<% next if @content.send(content_type.name.downcase.pluralize).empty? %>

View File

@ -3,6 +3,7 @@
<ul class="collection">
<% content.change_events.reverse.each do |change_event| %>
<li class="collection-item">
<% next if change_event.user.nil? %>
<%= (change_event.user == current_user) ? 'You' : link_to(change_event.user.name, change_event.user) %>
<%= change_event.action %> this <%= change_event.content_type.downcase %>
<span class="grey-text"><%= time_ago_in_words change_event.created_at %> ago</span>.
@ -25,6 +26,20 @@
old_value = change.first.blank? ? '(blank)' : change.first.to_s
new_value = change.second.blank? ? '(blank)' : change.second.to_s
visible_change = true
if key.start_with?('private')
visible_change = user_signed_in? && (
(content.is_a?(Universe) && content.user == current_user) ||
(content.respond_to?(:universe) && content.universe && content.universe.user == current_user) ||
(content.respond_to?(:universe) && content.universe.nil? && content.user == current_user)
)
end
unless visible_change
old_value = '(hidden)'
new_value = '(hidden)'
end
# Just for clarity / ease of mind to users -- we treat blank as private, but that's not obvious to them.
if key == 'privacy'
old_value = 'private' if old_value == '(blank)'
@ -46,4 +61,4 @@
<%= content.class.name %> <%= content.name %> was created by <%= link_to content.user.name, content.user %>
<span class='grey-text'><%= time_ago_in_words content.created_at %> ago</span>.
</li>
</ul>
</ul>

View File

@ -0,0 +1,37 @@
<%# Usage: render partial: 'content/display/contributors', locals: { content: content } %>
<ul class="collection">
<% content.contributors.each do |contributor| %>
<% next if contributor.user.nil? && (current_user != content.user) %>
<li class="collection-item avatar">
<i class="material-icons circle white-text">person</i>
<span class="title">
<%= contributor.user ? link_to(contributor.user.name, contributor.user) : "#{contributor.email} (invited)" %>
</span>
<p class="grey-text">
Invited <%= time_ago_in_words contributor.created_at %> ago
</p>
<% if current_user && content.user == current_user %>
<p>
<%= link_to 'Remove this contributor', remove_contributor_path(contributor.id),
class: 'js-remove-contributor',
method: 'delete',
remote: true,
data: { confirm: "Are you sure? They will no longer have contributor access to this universe." } %>
</p>
<% end %>
<span class="secondary-content"><i class="material-icons grey-text">group</i></span>
</li>
<% end %>
</ul>
<%= render partial: 'content/form/contributors/leave', locals: { content: content } %>
<script type="text/javascript">
$(document).ready(function() {
$('a.js-remove-contributor[data-remote]').on('ajax:success', function (e, data, status, xhr) {
// Remove the image from the UI
$(this).closest('.collection-item').fadeOut().remove();
});
})
</script>

View File

@ -1,7 +1,7 @@
<%
editing = defined?(editing) && editing
creating = defined?(creating) && creating
personal_content = (user_signed_in? && current_user.id == @content.user_id) || @content.new_record?
personal_content = @content.updatable_by?(current_user || User.new) || @content.new_record?
%>
<%# Primary FAB %>
@ -38,7 +38,7 @@
</a>
<% end %>
<% if @content.persisted? && editing %>
<% if @content.persisted? && editing && current_user.can_delete?(@content) %>
<%= link_to @content,
class: 'btn-floating btn-small halfway-fab waves-effect waves-light white delete left',
style: 'z-index: 500;',

View File

@ -27,6 +27,9 @@
# Show everything on create/edit forms
elsif category.name == 'changelog'
# Show changelog on show page
elsif category.name == 'contributors'
# Show contributors on show page if there are any to show
next if @content.contributors.empty?
elsif category.name == 'gallery'
# Only show gallery tab if the content has any images
next if @content.image_uploads.empty?
@ -48,6 +51,9 @@
<a href="#<%= category.name.gsub("'", '') %>_panel" class="<%= 'red-text' if category.label == 'Overview' %>">
<i class="material-icons left"><%= category.icon %></i>
<%= category.label.capitalize %>
<% if editing && category.label == 'Contributors' %>
<span class="new badge <%= Universe.color %>"></span>
<% end %>
</a>
</li>
<% end %>

View File

@ -0,0 +1,53 @@
<%# Usage: render partial: 'content/form/contributors', locals: { content: content } %>
<p>
Universes can have an unlimited number of collaborators.
</p>
<p>
When a user is added as a collaborator to a universe, they are allowed to:
<ul class="browser-default">
<li>View all content in that universe</li>
<li>Create new content in that universe</li>
<li>Edit any content in that universe</li>
</ul>
</p>
<p>
They are NOT allowed to:
<ul class="browser-default">
<li>Delete anyone else's content from that universe</li>
<li>Change the 'universe' field on any content in that universe</li>
<li>Change the privacy of any content in that universe</li>
<li>Invite additional contributors</li>
</ul>
</p>
<% if content.contributors.any? %>
<p>
The following users are enabled as contributors to this universe.
<% if user_signed_in? && content.user == current_user %>
You may remove their contributor privileges at any time.
<% end %>
</p>
<%= render partial: 'content/display/contributors', locals: { content: content } %>
<% end %>
<% if user_signed_in? && content.user == current_user %>
<p>
You may also add additional contributors to this universe by inviting them by email below.
</p>
<div>
<%= render partial: 'content/form/contributors/add', locals: { f: f, content: content } %>
</div>
<div>
<%= link_to_add_association "invite another", f,
:contributors,
partial: 'content/form/contributors/add',
render_options: { locals: { f: f, content: content }}
%>
</div>
<% end %>

View File

@ -1,5 +1,6 @@
<div id="<%= category.name.gsub("'", '') %>_panel" class="row panel">
<%# todo: break this out into a partial %>
<% if category.name == 'gallery' %>
<div>
<%= render partial: 'content/form/images/edit_list', locals: { content: content } %>
@ -8,7 +9,11 @@
<h5>
Upload more images
<small class="grey-text">
You have <%= Filesize.from("#{current_user.upload_bandwidth_kb}KB").pretty %> of bandwidth remaining.
<% if current_user.upload_bandwidth_kb > 0 %>
You have <%= Filesize.from("#{current_user.upload_bandwidth_kb}KB").pretty %> of bandwidth remaining.
<% else %>
You have no upload bandwidth remaining. Upgrade to Premium or delete some existing images for more.
<% end %>
</small>
</h5>
</div>
@ -24,6 +29,10 @@
</div>
<% end %>
<% if category.name == 'contributors' %>
<%= render partial: 'content/form/contributors', locals: { f: f, content: content } %>
<% end %>
<% # 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
@ -54,7 +63,7 @@
<% 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 %>
<%= render 'content/form/relation_input', f: f, attribute: field.name, label: field.label, relation: through_class %>
<% elsif field.name == 'archetype' %>
<div class="input-field input-select">
@ -65,7 +74,46 @@
<% 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]) %>
<% if !f.object.persisted? || (f.object.persisted? && f.object.universe && f.object.universe.user == current_user) || f.object.universe_id.nil? %>
<%# todo not like this %>
<%
valid_universes = []
show_premium_notice = false
if content.is_a?(Character) || content.is_a?(Location) || content.is_a?(Item)
valid_universes += current_user.universes
valid_universes += current_user.contributable_universes
else
# Premium content
if current_user.on_premium_plan?
valid_universes += current_user.universes
else
show_premium_notice = true
end
current_user.contributable_universes.each do |potential_universe|
if potential_universe.user.on_premium_plan?
valid_universes += [potential_universe]
end
end
end
%>
<%=
f.select field.name,
valid_universes.uniq.sort_by(&:name).map { |u| [u.name, u.id] }.compact,
include_blank: current_user.on_premium_plan? || content.is_a?(Character) || content.is_a?(Location) || content.is_a?(Item),
selected: (f.object.persisted? ? f.object.universe_id : session[:universe_id])
%>
<% if show_premium_notice %>
<div class="help-text">
<i class="material-icons" style="font-size: 90%">info</i>
While on a <%= link_to 'Starter plan', subscription_path %>, you can only create premium content in universes you're a contributor to.
</div>
<% end %>
<% else %>
<br />
<%= link_to(f.object.universe.name, f.object.universe) if f.object.universe %>
<% end %>
</div>
<% elsif field.name == 'attribute_category_id' %>
@ -94,13 +142,27 @@
</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.
<%#
This field is shown if and only if:
* A user is signed in AND
* That user is the owner of this universe, if it's a universe, or
* That user is the owner of this content's universe, if it's content
* That user is the owner of this content, if it's not in a universe
%>
<% if user_signed_in? && (
(content.is_a?(Universe) && content.user == current_user) ||
(content.respond_to?(:universe) && content.universe && content.universe.user == current_user) ||
(content.respond_to?(:universe) && content.universe.nil? && content.user == current_user)
)
%>
<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>
</div>
<% end %>
<% elsif field.name == 'item_type' %>
<% potential_item_types = t('weapon_types') + t('shield_types') + t('axe_types') + t('bow_types') +

View File

@ -5,7 +5,7 @@
<%= f.label attribute, I18n.translate(
"attributes.#{f.object.class.name.downcase}.#{attribute}",
scope: :activerecord,
default: attribute.humanize.capitalize
default: label.humanize.capitalize
) %>
</div>

View File

@ -0,0 +1,21 @@
<div class="nested-fields">
<%= f.fields_for :contributors do |ff| %>
<div class="row">
<div class="col s11">
<div class="input-field">
<%= ff.label 'Contributor email address' %><br />
<%= ff.text_field :email, name: 'contributors[email][]' %>
</div>
</div>
<div class="col s1">
<br />
<br />
<%= link_to_remove_association f do %>
<i class="material-icons red-text" style="position: relative; top: 10px; left: -15px;">close</i>
<% end %>
</div>
</div>
<% end %>
</div>

View File

@ -0,0 +1,15 @@
<%# Usage: render partial: 'content/form/contributors/leave', locals: { content: content } %>
<% if user_signed_in? && content.contributors.pluck(:user_id).include?(current_user.id) %>
<p class="grey-text">
You are a contributor to this universe. You may dismiss yourself as a contributor at any time, but will need to be reinvited to begin contributing again.
You will lose access to all content in this universe unless it is shared publicly, even if you created or edited it previously.
</p>
<p class="center">
<% my_contributor_object = Contributor.where(user: current_user, universe_id: content.id).first %>
<%= link_to 'Leave this universe', remove_contributor_path(my_contributor_object.id),
class: 'btn red lighten-3 black-text',
method: 'delete',
data: { confirm: "Are you sure? You will no longer have contributor access to this universe." } %>
</p>
<% end %>

View File

@ -1,6 +1,5 @@
<%
content_type_class = @content.build.class
content_type = content_type_class.name.downcase
content_type = @content_type_class.name.downcase
%>
<% content_for :sidebar_top do %>
@ -32,28 +31,28 @@
<% if @content.any? %>
<h4>
You've created <%= pluralize(@content.count, @content.content_name) %>
Your <%= pluralize(@content.count, content_type) %>
<% if @content.count > 0 %>
<small class="right">
<% if current_user.can_create? content_type_class %>
<% if current_user.can_create? @content_type_class %>
<%= link_to new_polymorphic_path(@content.build), class: "btn white #{content_type_class.color}-text" do %>
<%= link_to new_polymorphic_path(@content_type_class), class: "btn white #{@content_type_class.color}-text" do %>
+
<i class="material-icons <%= content_type_class.color %>-text tooltipped" data-position="bottom" data-tooltip="Create another <%= content_type_class.name %>">
<%= content_type_class.icon %>
<i class="material-icons <%= @content_type_class.color %>-text tooltipped" data-position="bottom" data-tooltip="Create another <%= @content_type_class.name %>">
<%= @content_type_class.icon %>
</i>
<% end %>
<% else %>
<%= link_to subscription_path, class: "btn grey lighten-3 #{content_type_class.color}-text tooltipped", data: {
<%= link_to subscription_path, class: "btn grey lighten-3 #{@content_type_class.color}-text tooltipped", data: {
position: 'bottom',
tooltip: "Upgrade to Premium to create more #{content_type_class.name.pluralize}",
tooltip: "Upgrade to Premium to create more #{@content_type_class.name.pluralize}",
delay: 50
} do %>
+
<i class="material-icons <%= content_type_class.color %>-text">
<%= content_type_class.icon %>
<i class="material-icons <%= @content_type_class.color %>-text">
<%= @content_type_class.icon %>
</i>
<% end %>
@ -61,25 +60,25 @@
</small>
<% end %>
</h4>
<%= render partial: 'content/list/list', locals: { content_list: @content, show_add_another_form: true } %>
<%= render partial: 'content/list/list', locals: { content_list: @content, content_type: @content_type_class, show_add_another_form: true } %>
<% elsif @content.empty? %>
<div class="center" style="margin: 50px 0">
<h4>You haven't created any <%= @content.content_name.pluralize %> yet!</h4>
<h4>You haven't created any <%= content_type.pluralize %> yet!</h4>
<h1>
<i class="material-icons <%= content_type_class.color %>-text" style="font-size: 200%">
<%= content_type_class.icon %>
<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}") %>
</p>
<% if current_user.can_create? content_type_class %>
<%= link_to "Create your first #{@content.content_name.humanize.downcase}", new_polymorphic_path(@content.build), class: "btn #{content_type_class.color}" %>
<% if current_user.can_create? @content_type_class %>
<%= link_to "Create your first #{content_type.humanize.downcase}", new_polymorphic_path(@content_type_class), class: "btn #{@content_type_class.color}" %>
<% else %>
<%= link_to "Upgrade to Premium to create your first #{@content.content_name.humanize.downcase}", subscription_path %>
<%= link_to "Upgrade to Premium to create your first #{content_type.humanize.downcase}", subscription_path %>
<% end %>
</div>

View File

@ -1,6 +1,4 @@
<%
content_type = content_list.any? ? content_list.first.class : content_list.build.class
%>
<%# Usage: render partial: 'content/list/list', locals: { content_list: @content, content_type: @content_type_class, show_add_another_form: true } %>
<ul class="collection <%= 'with-header' if title.present? %>" style='border: 0'>
<% if title.present? %>
@ -20,13 +18,32 @@
<%= link_to content.universe.name, content.universe %>
<% end %>
</small>
<% if user_signed_in? && (
(content.respond_to?(:universe) && content.universe && content.universe.user != current_user) ||
(content.is_a?(Universe) && content.user != current_user)
)
%>
<span class="new badge grey white-text tooltipped" style="float: inherit; margin: 0;" data-badge-caption="contributing"
data-position="bottom" data-delay="100" data-tooltip="You have been added as a contributor to this universe and its content."></span>
<% end %>
<% if user_signed_in? && (
(
content.respond_to?(:universe) && content.universe && content.universe.user == current_user &&
content.user != current_user
) ||
(content.is_a?(Universe) && content.contributors.include?(current_user))
)
%>
<span class="new badge grey white-text tooltipped" style="float: inherit; margin: 0;" data-badge-caption="from contributor"
data-position="bottom" data-delay="100" data-tooltip="This content was created by a contributor to your universe."></span>
<% end %>
</span>
<p class="content-description truncate">
<%= content.description %>
</p>
<p class='grey-text'>
<% if current_user && current_user == content.user %>
<% if user_signed_in? && content.updatable_by?(current_user) %>
<span class="timestamp tooltipped" data-position="bottom" data-delay="500" data-tooltip="<%= content.updated_at.strftime("%m/%d/%Y %H:%M") %>" style="font-size: 80%">
<i class="material-icons">mode_edit</i>
@ -43,7 +60,7 @@
<% end %>
</p>
<span class="secondary-content hide-on-med-and-up">
<% if current_user && current_user == content.user %>
<% if current_user && content.updatable_by?(current_user) %>
<%= link_to edit_polymorphic_path(content), class: 'js-edit-hover' do %>
<i class="material-icons">edit</i>
<% end %>
@ -53,8 +70,10 @@
<% end %>
<% if local_assigns[:show_add_another_form] && content_list.any? && current_user.can_create?(content_type) %>
<% unless content_type == Universe %>
<%= render partial: 'content/list/quick_add_form', locals: { content_type: content_list.build.class } %>
<% if content_type == Character || content_type == Location || content_type == Item || current_user.on_premium_plan? %>
<% unless content_type == Universe %>
<%= render partial: 'content/list/quick_add_form', locals: { content_type: content_type } %>
<% end %>
<% end %>
<% end %>

View File

@ -33,8 +33,20 @@
<%= render partial: 'content/display/changelog', locals: { content: content } %>
</div>
<% end %>
<% if category.name == 'contributors' %>
<div>
<%= render partial: 'content/display/contributors', locals: { content: content } %>
</div>
<% end %>
<% category.attribute_fields.each do |attribute| %>
<% next if attribute.name.start_with?("private") && (current_user.nil? || @content.user != current_user) %>
<% if attribute.name.start_with?("private") %>
<% next unless user_signed_in? && (
(content.is_a?(Universe) && content.user == current_user) ||
(content.respond_to?(:universe) && content.universe && content.universe.user == current_user) ||
(content.respond_to?(:universe) && content.universe.nil? && content.user == current_user)
)
%>
<% end %>
<%
custom_field = false
@ -82,10 +94,10 @@
<% end %>
</div>
<% if current_user.nil? || current_user.id != @content.user_id %>
<% if current_user.nil? || current_user.id != @content.user_id && (@content.respond_to?(:universe) && @content.universe && @content.universe.user_id != current_user.id) %>
<div class="card-action center">
<p>
<%= @content.user.name %> created and maintains this universe on Notebook.ai.
This <%= @content.class.name.downcase %> was created by <%= @content.user.name %> on Notebook.ai<% if @content.is_a?(Universe) && @content.contributors.any? %> with <%= pluralize @content.contributors.count, 'contributor' %><% end %>.
</p>
<%= link_to "See more from #{@content.user.name}", @content.user %>
<%= link_to "Create your own universe", root_url %>

View File

@ -0,0 +1,2 @@
<h1>Contributors#create</h1>
<p>Find me in app/views/contributors/create.html.erb</p>

View File

@ -0,0 +1,2 @@
<h1>Contributors#destroy</h1>
<p>Find me in app/views/contributors/destroy.html.erb</p>

View File

@ -6,13 +6,18 @@
<div class="card-content">
<h4>Personal information</h4>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
<%= f.label 'Name (visible on your profile and forum posts)' %><br />
<%= f.text_field :name, autofocus: true %>
</div>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true %>
<%= f.label 'Email (always completely private)' %><br />
<%= f.email_field :email %>
</div>
<div class="field">
<%= f.label 'Username (users can @mention you with your username on the forums)' %><br />
<%= f.text_field :username %>
</div>
</div>
</div>
@ -54,7 +59,7 @@
<div class="field">
<%= f.check_box :email_updates %>
<%= f.label :email_updates do %>
If this box is checked, you will receive occasional updates by email about new Notebook.ai features.
I want to receive occasional updates by email about new Notebook.ai features.
<% end %>
</div>
</div>

View File

@ -1,2 +0,0 @@
<h1>Emails#one_click_unsubscribe</h1>
<p>Find me in app/views/emails/one_click_unsubscribe.html.erb</p>

View File

@ -37,8 +37,10 @@
<li><%= link_to "religions.csv", religions_csv_path %></li>
<li><%= link_to "magics.csv", magics_csv_path %></li>
<li><%= link_to "languages.csv", languages_csv_path %></li>
<li><%= link_to "scenes.csv", scenes_csv_path %></li>
<li><%= link_to "groups.csv", groups_csv_path %></li>
<li><%= link_to "floras.csv", floras_csv_path %></li>
<li><%= link_to "scenes.csv", scenes_csv_path %></li>
</ul>
</div>
</div>

View File

@ -6,7 +6,7 @@
<i class="large material-icons">add</i>
</a>
<ul>
<% [Universe, Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Scene].reverse.each do |content_type| %>
<% [Universe, Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Scene, Flora].reverse.each do |content_type| %>
<% next unless current_user.can_create?(content_type) %>
<li>
<%= link_to new_polymorphic_path(content_type), class: "btn-floating #{content_type.color}" do %>

View File

@ -19,7 +19,7 @@
</li>
<li class="divider"></li>
<li>
<%= link_to privacy_policy_path do %>
<%= link_to main_app.privacy_policy_path do %>
<i class="material-icons left">verified_user</i>
Privacy policy
<% end %>
@ -28,7 +28,7 @@
<ul id="recent-content-dropdown" class="dropdown-content">
<% if user_signed_in? %>
<% current_user.content.values.flatten.compact.sort_by(&:updated_at).last(10).reverse.each do |content| %>
<% current_user.content.values.reject(&:empty?).flatten.compact.sort_by(&:updated_at).last(10).reverse.each do |content| %>
<% content_class = content.class %>
<li>
<%= link_to content, class: "#{content_class.color}-text" do %>
@ -57,14 +57,14 @@
</li>
<li>
<%= link_to subscription_path do %>
<%= link_to main_app.subscription_path do %>
<i class="material-icons left">credit_card</i>
Account billing
<span class="badge right">&nbsp;</span>
<% end %>
</li>
<li>
<%= link_to edit_user_registration_path do %>
<%= link_to main_app.edit_user_registration_path do %>
<i class="material-icons left">settings</i>
Account settings
<span class="badge right">&nbsp;</span>
@ -75,13 +75,13 @@
<!--
<li>
<%= link_to friends_landing_path do %>
<%= link_to main_app.friends_landing_path do %>
<i class="material-icons left">people</i>
Community Drive
<% end %>
</li>
<li>
<%= link_to community_voting_path do %>
<%= link_to main_app.community_voting_path do %>
<i class="material-icons left">star_outline</i>
Vote on features
<% end %>
@ -90,20 +90,20 @@
<li class="divider"></li>
-->
<li>
<%= link_to prompts_path do %>
<%= link_to main_app.prompts_path do %>
<i class="material-icons left">lightbulb_outline</i>
Worldbuilding prompts
<% end %>
</li>
<li>
<%= link_to notebook_export_path do %>
<%= link_to main_app.notebook_export_path do %>
<i class="material-icons left">file_download</i>
Notebook downloads
<% end %>
</li>
<li class="divider"></li>
<li>
<%= link_to destroy_user_session_path do %>
<%= link_to main_app.destroy_user_session_path do %>
<i class="material-icons left">power_settings_new</i>
Sign out
<% end %>
@ -111,20 +111,20 @@
</ul>
<ul id="login-slide" class="side-nav">
<li><%= link_to 'Sign in', new_user_session_path %></li>
<li><%= link_to 'Sign up', new_user_registration_path %></li>
<li><%= link_to 'Sign in', main_app.new_user_session_path %></li>
<li><%= link_to 'Sign up', main_app.new_user_registration_path %></li>
<li class="divider"></li>
<li><%= link_to 'Report a problem', 'https://docs.google.com/forms/d/e/1FAIpQLSe0jnqJlcPJDqwogGere5j8-8F1nSGGYkzbsI-XkOeMnGwLrA/viewform', target: '_new' %></li>
<li><%= link_to 'Request a feature', 'https://docs.google.com/forms/d/e/1FAIpQLScOhSMdDqWqJu9HGPQdmd0R9s90_AcSo0g6_Nc1qNjQIbSaXA/viewform', target: '_new' %></li>
<li><%= link_to 'Leave feedback', 'https://docs.google.com/forms/d/e/1FAIpQLScZWEVMgm8hBWIIVj1LPzo0GqflmWUrLQlc4TAYqsaS087oAA/viewform', target: '_new' %></li>
<li class="divider"></li>
<li><%= link_to 'Privacy policy', privacy_policy_path %></li>
<li><%= link_to 'Privacy policy', main_app.privacy_policy_path %></li>
</ul>
<div class="navbar-fixed">
<nav class="light-blue">
<div class="nav-wrapper">
<%= link_to 'Notebook.ai', root_url, class: 'brand-logo center' %>
<%= link_to 'Notebook.ai', main_app.root_url, class: 'brand-logo center' %>
<ul class="right">
<% if user_signed_in? %>
@ -140,14 +140,14 @@
</a>
</li>
<% else %>
<li class="hide-on-med-and-down"><%= link_to 'Sign in', new_user_session_path %></li>
<li class="hide-on-med-and-down"><%= link_to 'Sign up', new_user_registration_path %></li>
<li class="hide-on-med-and-down"><%= link_to 'Sign in', main_app.new_user_session_path %></li>
<li class="hide-on-med-and-down"><%= link_to 'Sign up', main_app.new_user_registration_path %></li>
<% end %>
</ul>
<ul class="left">
<% if user_signed_in? %>
<li>
<% if current_user && current_user.universes.length > 0 %>
<% if current_user && (current_user.universes.count > 0 || current_user.contributable_universes.count > 0) %>
<%= render partial: 'universes/picker' %>
<% end %>
</li>
@ -175,7 +175,7 @@
<div id="search">
<button type="button" class="close red">×</button>
<%= form_tag search_path, method: :get do %>
<%= form_tag main_app.search_path, method: :get do %>
<input type="search" name="q" value="" placeholder="search your notebook" />
<button type="submit" class="btn btn-primary blue">Search everything</button>
<% end %>

View File

@ -31,7 +31,7 @@
You can upload images to any content from the "Gallery" tab while editing it.
</p>
<p>
Free users have 50MB of space, and premium users have a whopping 1GB.
Free users have 50MB of space, and premium users have a whopping 10GB.
<% if current_user && current_user.selected_billing_plan_id == BillingPlan.find_by(stripe_plan_id: 'free-for-life').id %>
Because you were here at Notebook.ai's launch, <strong>we've quintupled your space from 50MB to 250MB. Thank you!</strong>
<% end %>

View File

@ -31,6 +31,31 @@
<% end %>
</li>
<li>
<%= link_to thredded_path do %>
<i class="material-icons grey-text">
forum
</i>
Discussions
<%
unread_threads = Thredded::Topic
.followed_by(current_user)
.unread(current_user)
.count
private_messages = Thredded::PrivateTopic
.for_user(current_user)
.unread(current_user)
.count
%>
<% if unread_threads > 0 || private_messages > 0 %>
<span class="badge <%= 'blue white-text' if private_messages > 0 %>">
<%= unread_threads %><%= "/#{private_messages}" if private_messages > 0 %>
</span>
<% end %>
<% end %>
</li>
<li>
<%= link_to notes_path do %>
<i class="material-icons grey-text">
@ -59,7 +84,12 @@
if base_content_type != Universe && @universe_scope
@universe_scope.send(base_content_type.name.downcase.pluralize).count
else
current_user.send(base_content_type.name.downcase.pluralize).count
pluralized_name = base_content_type.name.downcase.pluralize
(
current_user.send(pluralized_name) +
current_user.send("contributable_#{pluralized_name}") +
(base_content_type == Universe ? [] : current_user.universes.flat_map { |universe| universe.send(pluralized_name) })
).uniq.count
end
%>
</span>
@ -83,7 +113,12 @@
if @universe_scope
@universe_scope.send(extended_content_type.name.downcase.pluralize).count
else
current_user.send(extended_content_type.name.downcase.pluralize).count
pluralized_name = extended_content_type.name.downcase.pluralize
(
current_user.send(pluralized_name) +
current_user.send("contributable_#{pluralized_name}") +
current_user.universes.flat_map { |universe| universe.send(pluralized_name) }
).uniq.count
end
%>
</span>
@ -116,7 +151,12 @@
if @universe_scope
@universe_scope.send(extended_content_type.name.downcase.pluralize).count
else
current_user.send(extended_content_type.name.downcase.pluralize).count
pluralized_name = extended_content_type.name.downcase.pluralize
(
current_user.send(pluralized_name) +
current_user.send("contributable_#{pluralized_name}") +
current_user.universes.flat_map { |universe| universe.send(pluralized_name) }
).uniq.count
end
%>
</span>
@ -132,4 +172,4 @@
$(document).ready(function () {
$(".button-collapse").sideNav();
})
</script>
</script>

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title><%= yield :thredded_page_title %> | Notebook.ai</title>
<meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' name='viewport' />
<%= stylesheet_link_tag 'thredded', 'data-turbolinks-track': 'reload' %>
<%= csrf_meta_tag %>
<%= javascript_include_tag 'thredded',
async: !Rails.application.config.assets.debug,
defer: true,
'data-turbolinks-track': 'reload'%>
<%== Gravatar.prefetch_dns %>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
</head>
<body>
<%= render 'thredded/shared/nav/bar' %>
<%= yield %>
<%= render 'layouts/ganalytics' %>
</body>
</html>

View File

@ -0,0 +1,5 @@
<html>
<body>
<%= yield %>
</body>
</html>

View File

@ -0,0 +1 @@
<%= yield %>

View File

@ -17,7 +17,7 @@
<%
s_width = 12
m_width = 6
l_width = 3
l_width = 4
if current_user.content_count > 0
# If we're losing 2 columns to the sidebar (if any content exists), only show 2 content types per row
@ -27,8 +27,20 @@
<div class="row">
<% [:universe, :character, :location, :item, :creature, :race, :religion, :group, :magic, :language, :flora, :scene].each do |type| %>
<% content_list = current_user.send(type.to_s.pluralize) %>
<% content_list = content_list.in_universe(@universe_scope) if (@universe_scope.present? && type != :universe) %>
<%
pluralized_content_name = type.to_s.pluralize
if @universe_scope.present? && type != :universe
content_list = @universe_scope.send(pluralized_content_name)
else
content_list = current_user.send(pluralized_content_name) + current_user.send("contributable_#{pluralized_content_name}")
unless type == :universe
content_list.concat(current_user.universes.flat_map { |universe| universe.send(pluralized_content_name) }).uniq!
end
end
%>
<% next if !current_user.can_create?(type.to_s.capitalize.constantize) && content_list.empty? %>
<div class="dashboard-row col <%= "s#{s_width} m#{m_width} l#{l_width}" %>">
<%= render partial: 'content/cards/in_universe_content_list', locals: { content_type: type, content_list: content_list } %>

View File

@ -220,7 +220,7 @@ body {
</p>
<p class="light">
Free users start with 50MB of storage space that can be used to upload any size of image. Premium users are boosted an additional
1GB of storage space. You can delete any image at any time to reclaim and reuse its space.
10GB of storage space. You can delete any image at any time to reclaim and reuse its space.
</p>
<div class="tip card-panel blue lighten-5 light">
Tip: Uploading images is a great way to save the maps of your world. They're highlighted at the top of their Location page,
@ -368,7 +368,7 @@ body {
</p>
<p class="light">
<strong>Premium memberships are $9.00, billed monthly, and allow you to create unlimited amounts of any type of content.</strong>
Additionally, a Premium subscription also increases your image upload storage from 50MB to 1GB.
Additionally, a Premium subscription also increases your image upload storage from 50MB to 10GB.
</p>
</div>
<div class="col s3">
@ -643,4 +643,3 @@ body {
</div>
</div>
</div>

View File

@ -125,7 +125,7 @@
A huge thank you to everyone that participated in the community drive!
</p>
<p style="padding-top: 20px;">
Our raffle winner has been notified (check your email!) and we've added 1GB of storage space to all Premium user accounts
Our raffle winner has been notified (check your email!) and we've added 10GB of storage space to all Premium user accounts
from now until forever as a thank you.
</p>
<p style="padding-top: 20px;">

View File

@ -217,7 +217,7 @@ body {
</p>
<p class="light">
Free users start with 50MB of storage space that can be used to upload any size of image. Premium users are boosted an additional
1GB of storage space. You can delete any image at any time to reclaim and reuse its space.
10GB of storage space. You can delete any image at any time to reclaim and reuse its space.
</p>
<div class="tip card-panel blue lighten-5 light">
Tip: Uploading images is a great way to save the maps of your world. They're highlighted at the top of their Location page,
@ -373,7 +373,7 @@ body {
</p>
<p class="light">
<strong>Premium memberships are $9.00, billed monthly, and allow you to create unlimited amounts of any type of content.</strong>
Additionally, a Premium subscription also increases your image upload storage from 50MB to 1GB.
Additionally, a Premium subscription also increases your image upload storage from 50MB to 10GB.
</p>
</div>
<div class="col s3">

View File

@ -213,7 +213,7 @@ body {
</p>
<p class="light">
Free users start with 50MB of storage space that can be used to upload any size of image. Premium users are boosted an additional
1GB of storage space. You can delete any image at any time to reclaim and reuse its space.
10GB of storage space. You can delete any image at any time to reclaim and reuse its space.
</p>
<div class="tip card-panel blue lighten-5 light">
Tip: Uploading images is a great way to save the maps of your world. They're highlighted at the top of their Location page,
@ -371,7 +371,7 @@ body {
</p>
<p class="light">
<strong>Premium memberships are $9.00, billed monthly, and allow you to create unlimited amounts of any type of content.</strong>
Additionally, a Premium subscription also increases your image upload storage from 50MB to 1GB.
Additionally, a Premium subscription also increases your image upload storage from 50MB to 10GB.
</p>
</div>
<div class="col s3">
@ -481,7 +481,7 @@ body {
<th>Upload images</th>
<td>Up to 50MB</td>
<td>-</td>
<td>Up to 1GB</td>
<td>Up to 10GB</td>
</tr>
<tr>
<th>Price</th>

View File

@ -32,33 +32,39 @@
<div class="col s12 m4">
<div class="icon-block center hoverable">
<h2 class="blue-text"><i class="material-icons">create</i></h2>
<h5>For writers</h5>
<h5><%= t('marketing.landing_page.cta.writers.header') %></h5>
<p class="light">
Authors use Notebook.ai to create rich, immersive worlds to set their novels, stories, and plays in.
<%= t('marketing.landing_page.cta.writers.body') %>
</p>
<%= link_to 'Learn more', writers_landing_path, class: 'btn blue' %>
<%= link_to t('marketing.landing_page.cta.writers.button'),
writers_landing_path,
class: 'btn blue' %>
</div>
</div>
<div class="col s12 m4">
<div class="icon-block center hoverable">
<h2 class="blue-text"><i class="material-icons">gavel</i></h2>
<h5>For roleplayers</h5>
<h5><%= t('marketing.landing_page.cta.roleplayers.header') %></h5>
<p class="light">
Dungeon masters use Notebook.ai to build worlds for their campaigns that can be shared with players.
<%= t('marketing.landing_page.cta.roleplayers.body') %>
</p>
<%= link_to 'Learn more', roleplayers_landing_path, class: 'btn blue' %>
<%= link_to t('marketing.landing_page.cta.roleplayers.button'),
roleplayers_landing_path,
class: 'btn blue' %>
</div>
</div>
<div class="col s12 m4">
<div class="icon-block center hoverable">
<h2 class="blue-text"><i class="material-icons">brush</i></h2>
<h5>For designers</h5>
<h5><%= t('marketing.landing_page.cta.designers.header') %></h5>
<p class="light">
Game design teams use Notebook.ai to collaborate and keep every aspect of their world in sync.
<%= t('marketing.landing_page.cta.designers.body') %>
</p>
<%= link_to 'Learn more', designers_landing_path, class: 'btn blue' %>
<%= link_to t('marketing.landing_page.cta.designers.button'),
designers_landing_path,
class: 'btn blue' %>
</div>
</div>
</div>
@ -217,11 +223,11 @@
<div class="section">
<div class="row">
<%
content_type_pool = [Universe, Scene, Creature, Magic, Group, Language, Race, Religion]
content_type_pool = [Universe, Scene, Group, Creature, Magic, Language, Race, Religion, Flora]
%>
<% content_type_pool.each do |content_type| %>
<div class="col l3 m6 s12">
<div class="col l4 m6 s12">
<%= render partial: 'cards/intros/content_type_intro', locals: { content_type: content_type } %>
</div>
<% end %>
@ -306,6 +312,7 @@
<li><%= t('marketing.landing_page.pricing.premium_plan.magics_html') %></li>
<li><%= t('marketing.landing_page.pricing.premium_plan.races_html') %></li>
<li><%= t('marketing.landing_page.pricing.premium_plan.religions_html') %></li>
<li><%= t('marketing.landing_page.pricing.premium_plan.floras_html') %></li>
<li><%= t('marketing.landing_page.pricing.premium_plan.scenes_html') %></li>
</ul>
<div class="row center">

View File

@ -3,7 +3,7 @@
<% active_stripe_plan = @active_billing_plan.stripe_plan_id %>
<p class="right hide-on-small-only">
<% [Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Scene].each do |content_class| %>
<% [Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Flora, Scene].each do |content_class| %>
<% can_create = current_user.can_create?(content_class) %>
<% edit_message = current_user.send(content_class.name.pluralize.downcase).any? && !can_create %>
<i class="material-icons <%= can_create ? content_class.color : 'grey' %>-text tooltipped" style="font-size: 240%" data-position="bottom" data-delay="0" data-tooltip="You <%= can_create ? 'can' : 'cannot' %> create new <%= content_class.name.pluralize.downcase %>.<%= ' You can edit your existing ones.' if edit_message %>">
@ -60,6 +60,9 @@
Create <strong>core</strong> page types
</li>
<li class="center">
<i class="material-icons <%= Universe.color %>-text tooltipped" style="font-size: 240%" data-position="bottom" data-delay="0" data-tooltip="Create up to 5 <%= Universe.name.pluralize %>">
<%= Universe.icon %>
</i>
<% [Character, Location, Item].each do |content_class| %>
<i class="material-icons <%= content_class.color %>-text tooltipped" style="font-size: 240%" data-position="bottom" data-delay="0" data-tooltip="Create unlimited <%= content_class.name.pluralize %>">
<%= content_class.icon %>
@ -98,15 +101,15 @@
</li>
<li>
<i class="material-icons green-text">check</i>
Upload <strong>1GB</strong> of images
Upload <strong>10GB</strong> of images
</li>
<li>
<i class="material-icons green-text">check</i>
Create <strong>all 10</strong> page types
Create <strong>all 12</strong> page types
</li>
<li class="center">
<% [Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Scene].each_with_index do |content_class, i| %>
<%= '<br />'.html_safe if i % 5 == 0 && i != 0 %>
<% [Universe, Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Flora, Scene].each_with_index do |content_class, i| %>
<%= '<br />'.html_safe if i % 4 == 0 && i != 0 %>
<i class="material-icons <%= content_class.color %>-text tooltipped" style="font-size: 240%" data-position="bottom" data-delay="0" data-tooltip="Create unlimited <%= content_class.name.pluralize %>">
<%= content_class.icon %>
</i>
@ -145,15 +148,15 @@
</li>
<li>
<i class="material-icons green-text">check</i>
Upload <strong>1GB</strong> of images
Upload <strong>10GB</strong> of images
</li>
<li>
<i class="material-icons green-text">check</i>
Create <strong>all 10</strong> page types
Create <strong>all 12</strong> page types
</li>
<li class="center">
<% [Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Scene].each_with_index do |content_class, i| %>
<%= '<br />'.html_safe if i % 5 == 0 && i != 0 %>
<% [Universe, Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Flora, Scene].each_with_index do |content_class, i| %>
<%= '<br />'.html_safe if i % 4 == 0 && i != 0 %>
<i class="material-icons <%= content_class.color %>-text tooltipped" style="font-size: 240%" data-position="bottom" data-delay="0" data-tooltip="Create unlimited <%= content_class.name.pluralize %>">
<%= content_class.icon %>
</i>
@ -192,15 +195,15 @@
</li>
<li>
<i class="material-icons green-text">check</i>
Upload <strong>1GB</strong> of images
Upload <strong>10GB</strong> of images
</li>
<li>
<i class="material-icons green-text">check</i>
Create <strong>all 10</strong> page types
Create <strong>all 12</strong> page types
</li>
<li class="center">
<% [Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Scene].each_with_index do |content_class, i| %>
<%= '<br />'.html_safe if i % 5 == 0 && i != 0 %>
<% [Universe, Character, Location, Item, Creature, Race, Religion, Group, Magic, Language, Flora, Scene].each_with_index do |content_class, i| %>
<%= '<br />'.html_safe if i % 4 == 0 && i != 0 %>
<i class="material-icons <%= content_class.color %>-text tooltipped" style="font-size: 240%" data-position="bottom" data-delay="0" data-tooltip="Create unlimited <%= content_class.name.pluralize %>">
<%= content_class.icon %>
</i>
@ -246,27 +249,7 @@
</div>
<div class="row">
<div class="col s6">
<div class="card">
<div class="card-content">
<div class="card-title">Notebook.ai Community Drive</div>
<div class="row">
<div class="col s12">
<p>
For a limited time, we're holding a <%= link_to 'Community Drive', friends_landing_path %> for referring friends
to Notebook.ai. With each friend you refer that signs up for a Premium subscription, you'll gain +100 MB storage space,
two more entries in our upcoming raffle, and two more <%= link_to 'feature votes', community_voting_path %> to decide what
Notebook.ai's first community-first feature will be.
</p><br />
<div class="center">
<%= link_to 'See community progress', friends_landing_path, class: 'btn blue' %>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col s6">
<div class="col s12">
<div class="card">
<div class="card-content">
<div class="card-title">Your referral code & stats</div>
@ -282,7 +265,7 @@
</p>
<ul>
<% referrals = current_user.referrals %>
<% converted_referrals = referrals.map(&:referree).select { |user| [2, 3, 4, 5, 6].include? user.selected_billing_plan_id } %>
<% converted_referrals = referrals.map(&:referree).select(&:on_premium_plan?) %>
<li><strong><%= pluralize referrals.count, 'user' %></strong> <%= referrals.count == 1 ? 'has' : 'have' %> signed up with your referral code.</li>
<% if converted_referrals.any? %>
<li>
@ -291,14 +274,6 @@
<% else %>
<li>None have signed up for Premium yet, however.</li>
<% end %>
<li>You've earned <strong><%= pluralize current_user.votes.count, 'feature vote' %></strong> and have voted <%= pluralize current_user.votes.where.not(votable_id: nil).count, 'time' %>.
<% if current_user.votes.where(votable_id: nil).any? %>
<%= link_to 'Vote now!', community_voting_path %>
<% else %>
<%= link_to 'See votes so far.', community_voting_path %>
<% end %>
</li>
<li>You've earned <strong><%= pluralize current_user.raffle_entries.count, 'entry' %></strong> in <%= link_to "Notebook.ai's raffle", friends_landing_path %>, happening on June 15<sup>th</sup>, 2017.</li>
</ul>
</div>
</div>
@ -433,4 +408,4 @@
</tbody>
</table>
</div>
</div>
</div>

View File

@ -0,0 +1 @@
<li class="<%= category.name.underscore %>"><%= category.name %></li>

View File

@ -0,0 +1,6 @@
<% content_for :thredded_page_title, @message %>
<% content_for :thredded_page_id, 'thredded--error-forbidden' %>
<% content_for :thredded_breadcrumbs, render('thredded/shared/breadcrumbs') %>
<%= thredded_page do %>
<p class="thredded--alert thredded--alert-warning"><%= @message %></p>
<% end %>

View File

@ -0,0 +1,6 @@
<% content_for :thredded_page_title, @message %>
<% content_for :thredded_page_id, 'thredded--error-not-found' %>
<% content_for :thredded_breadcrumbs, render('thredded/shared/breadcrumbs') %>
<%= thredded_page do %>
<p class="thredded--alert thredded--alert-danger"><%= @message %></p>
<% end %>

View File

@ -0,0 +1,11 @@
<%# Link to the "First" page
- available local variables
url: url to the first page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<span class="first">
<%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, :remote => remote %>
</span>

View File

@ -0,0 +1,8 @@
<%# Non-link tag that stands for skipped pages...
- available local variables
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<span class="page gap"><%= t('views.pagination.truncate').html_safe %></span>

View File

@ -0,0 +1,11 @@
<%# Link to the "Last" page
- available local variables
url: url to the last page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<span class="last">
<%= link_to_unless current_page.last?, t('views.pagination.last').html_safe, url, :remote => remote %>
</span>

View File

@ -0,0 +1,11 @@
<%# Link to the "Next" page
- available local variables
url: url to the next page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<span class="next">
<%= link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, :rel => 'next', :remote => remote %>
</span>

View File

@ -0,0 +1,12 @@
<%# Link showing page number
- available local variables
page: a page object for "this" page
url: url to this page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<span class="page<%= ' current' if page.current? %>">
<%= link_to_unless page.current?, page, url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil} %>
</span>

View File

@ -0,0 +1,23 @@
<%# The container tag
- available local variables
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
paginator: the paginator that renders the pagination tags inside
-%>
<%= paginator.render do -%>
<nav class="pagination thredded--pagination">
<%= first_page_tag unless current_page.first? %>
<%= prev_page_tag unless current_page.first? %>
<% each_page do |page| -%>
<% if page.left_outer? || page.right_outer? || page.inside_window? -%>
<%= page_tag page %>
<% elsif !page.was_truncated? -%>
<%= gap_tag %>
<% end -%>
<% end -%>
<%= next_page_tag unless current_page.last? %>
<%= last_page_tag unless current_page.last? %>
</nav>
<% end -%>

View File

@ -0,0 +1,11 @@
<%# Link to the "Previous" page
- available local variables
url: url to the previous page
current_page: a page object for the currently displayed page
total_pages: total number of pages
per_page: number of items to fetch per page
remote: data-remote
-%>
<span class="prev">
<%= link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, :rel => 'prev', :remote => remote %>
</span>

View File

@ -0,0 +1,28 @@
<% content_for :thredded_page_title, t('thredded.messageboard_group.create') %>
<% content_for :thredded_page_id, 'thredded--messageboard-groups-new' %>
<% content_for :thredded_breadcrumbs do %>
<ul class="thredded--navigation-breadcrumbs">
<li class="thredded--navigation-breadcrumbs--item-no-link">
<%= t('thredded.messageboard_group.create') %>
</li>
</ul>
<% end %>
<%= thredded_page do %>
<section class="thredded--main-section">
<%= form_for @messageboard_group, html: { class: 'thredded--form' } do |f| %>
<ul class="thredded--form-list">
<li>
<%= f.label :name %>
<%= f.text_field :name, required: true %>
</li>
<li>
<%= f.submit t('thredded.messageboard_group.create'),
class: 'thredded--form--submit',
'data-disable-with' => t('thredded.messageboard_group.form.create_btn_submitting') %>
</li>
</ul>
<% end %>
</section>
<% end %>

View File

@ -0,0 +1,24 @@
<%= form_for messageboard, html: { class: 'thredded--form' } do |f| %>
<ul class="thredded--form-list">
<li>
<%= f.label t('thredded.messageboard.form.title_label') %>
<%= f.text_field :name, required: true %>
</li>
<li>
<%= f.label t('thredded.messageboard.form.description_label') %>
<%= f.text_field :description %>
</li>
<li>
<%= f.label t('thredded.messageboard.form.messageboard_group_id_label') %>
<%= f.collection_select :messageboard_group_id, Thredded::MessageboardGroup.all, :id, :name,
include_blank: t('thredded.messageboard.form.no_group') %>
</li>
<li>
<%= f.submit @messageboard.persisted? ? t('thredded.messageboard.update') : t('thredded.messageboard.create'),
class: 'thredded--form--submit',
'data-disable-with' => @messageboard.persisted? ?
t('thredded.messageboard.form.update_btn_submitting') :
t('thredded.messageboard.form.create_btn_submitting') %>
</li>
</ul>
<% end %>

View File

@ -0,0 +1,24 @@
<% if policy(messageboard).read? %>
<%= view_hooks.messageboards_index.messageboard.render self, messageboard: messageboard do %>
<%= link_to messageboard_topics_path(messageboard), class: 'thredded--messageboard' do %>
<header class="thredded--messageboard--header">
<h2 class="thredded--messageboard--title"><%= messageboard.name %></h2>
<h3 class="thredded--messageboard--meta">
<%= t 'thredded.messageboard.topics_and_posts_counts',
topics_count: number_with_delimiter(messageboard.topics_count),
posts_count: number_with_delimiter(messageboard.posts_count) %>
</h3>
</header>
<p class="thredded--messageboard--description"><%= messageboard.description %></p>
<% if messageboard.last_topic %>
<p class="thredded--messageboard--byline">
<%= t 'thredded.messageboard.last_updated_by_html',
time_ago: time_ago(messageboard.last_topic.last_post_at),
user: messageboard.last_user.thredded_display_name %>
</p>
<% end %>
<% end %>
<% end %>
<% end %>

View File

@ -0,0 +1,15 @@
<% content_for :thredded_page_title, t('thredded.nav.edit_messageboard') %>
<% content_for :thredded_page_id, 'thredded--messageboard-edit' %>
<% content_for :thredded_breadcrumbs do %>
<ul class="thredded--navigation-breadcrumbs">
<li class="thredded--navigation-breadcrumbs--item-no-link">
<%= t('thredded.nav.edit_messageboard') %>
</li>
</ul>
<% end %>
<%= thredded_page do %>
<section class="thredded--main-section">
<%= render 'form', messageboard: @messageboard %>
</section>
<% end %>

View File

@ -0,0 +1,34 @@
<% content_for :thredded_page_title, t('thredded.messageboard.index.page_title') %>
<% content_for :thredded_page_id, 'thredded--messageboards-index' %>
<% content_for :thredded_breadcrumbs, render('thredded/shared/breadcrumbs') %>
<%= thredded_page do %>
<%= view_hooks.messageboards_index.container.render self, groups: @groups do %>
<section class="thredded--main-section thredded--messageboards">
<%= view_hooks.messageboards_index.list.render self, groups: @groups do %>
<% @groups.each do |group| %>
<% if group.name.present? %>
<h3 class="thredded--messageboards-group--title"><%= group.name %></h3>
<% end %>
<div class="thredded--messageboards-group">
<%= view_hooks.messageboards_index.group.render self, group: group do %>
<%= render group.messageboards %>
<% end %>
</div>
<% end %>
<% end %>
<div class="thredded--messageboards--actions">
<% if policy(Thredded::Messageboard.new).create? %>
<a class="thredded--button" href="<%= new_messageboard_path %>" rel="nofollow">
<%= t('thredded.messageboard.create') %>
</a>
<% end %>
<% if policy(Thredded::MessageboardGroup.new).create? %>
<a class="thredded--button" href="<%= new_messageboard_group_path %>" rel="nofollow">
<%= t('thredded.messageboard_group.create') %>
</a>
<% end %>
</div>
</section>
<% end %>
<% end %>

View File

@ -0,0 +1,15 @@
<% content_for :thredded_page_title, t('thredded.messageboard.create') %>
<% content_for :thredded_page_id, 'thredded--messageboards-new' %>
<% content_for :thredded_breadcrumbs do %>
<ul class="thredded--navigation-breadcrumbs">
<li class="thredded--navigation-breadcrumbs--item-no-link">
<%= t('thredded.messageboard.create') %>
</li>
</ul>
<% end %>
<%= thredded_page do %>
<section class="thredded--main-section">
<%= render 'form', messageboard: @messageboard %>
</section>
<% end %>

View File

@ -0,0 +1,19 @@
<% content_for :thredded_main_navigation do %>
<nav class="thredded--moderation-navigation">
<ul class="thredded--moderation-navigation--items">
<li class="thredded--moderation-navigation--item thredded--moderation-navigation--pending">
<%= link_to t('thredded.nav.moderation_pending'), pending_moderation_path %>
</li>
<li class="thredded--moderation-navigation--item thredded--moderation-navigation--history">
<%= link_to t('thredded.nav.moderation_history'), moderation_history_path %>
</li>
<li class="thredded--moderation-navigation--item thredded--moderation-navigation--activity">
<%= link_to t('thredded.nav.moderation_activity'), moderation_activity_path %>
</li>
<li class="thredded--moderation-navigation--item thredded--moderation-navigation--users">
<%= link_to t('thredded.nav.moderation_users'), users_moderation_path %>
</li>
</ul>
<%= render 'users_search_form' %>
</nav>
<% end %>

View File

@ -0,0 +1,19 @@
<% post, content = post_and_content if local_assigns.key?(:post_and_content) %>
<%= content_tag :article, id: dom_id(post), class: 'thredded--post thredded--post-moderation' do %>
<%= render 'thredded/posts_common/actions', post: post %>
<%= render 'thredded/posts_common/header_with_user_and_topic',
post: post,
post_user_link: if post.user
link_to(post.user.thredded_display_name, user_moderation_path(post.user.id))
else
content_tag :em, t('thredded.null_user_name')
end
%>
<%= content || render('thredded/posts/content', post: post) %>
<% if post.blocked? %>
<p class="thredded--alert thredded--alert-danger">
<%= render 'thredded/shared/content_moderation_blocked_state', moderation_record: post.last_moderation_record %>
</p>
<% end %>
<%= render 'post_moderation_actions', post: post %>
<% end %>

View File

@ -0,0 +1,12 @@
<div class="thredded--post-moderation-actions">
<% unless post.approved? %>
<%= button_to t('thredded.moderation.approve_btn'), moderate_post_path,
class: 'thredded--button',
params: { id: post.to_model.id, moderation_state: 'approved' } %>
<% end %>
<% unless post.blocked? %>
<%= button_to t('thredded.moderation.block_btn'), moderate_post_path,
class: 'thredded--button',
params: { id: post.to_model.id, moderation_state: 'blocked' } %>
<% end %>
</div>

View File

@ -0,0 +1,46 @@
<%
record = post_moderation_record
post = record.post
if post
post_view = Thredded::PostView.new(post, policy(post))
end
moderation_state_notice_args = {
moderator: user_link(record.moderator),
time_ago: time_ago(record.created_at)
}
%>
<article class="thredded--post-moderation-record thredded--post-moderation-record-<%= record.moderation_state %>">
<header class="thredded--post-moderation-record--header">
<p class="thredded--post-moderation-record--moderation-state-notice">
<% if record.approved? %>
<%= t('thredded.moderation.post_approved_html', moderation_state_notice_args) %>
<% elsif record.blocked? %>
<%= t('thredded.moderation.post_blocked_html', moderation_state_notice_args) %>
<% end %>
</p>
<% if post && post.content != record.post_content %>
<p class="thredded--post-moderation-record--content-changed-notice">
<%= t('thredded.moderation.posts_content_changed_since_moderation_html', post_url: post_permalink_path(post)) %>
</p>
<% end %>
<%= content_tag :em, t('thredded.moderation.post_deleted_notice') unless post %>
</header>
<article class="thredded--post">
<% post_user_link = capture do %>
<% if record.post_user %>
<%= link_to record.post_user.thredded_display_name, user_moderation_path(record.post_user.id) %>
<% else %>
<%= safe_join [record.post_user_name, content_tag(:em, t('thredded.null_user_name'))].compact, ', ' %>
<% end %>
<% end %>
<% if post %>
<%= render 'thredded/posts_common/header_with_user_and_topic', post: post_view, post_user_link: post_user_link %>
<% else %>
<header><h2 class="thredded--post--user"><%= post_user_link %></h2></header>
<% end %>
<div class="thredded--post--content">
<%= Thredded::ContentFormatter.new(self).format_content(record.post_content) %>
</div>
</article>
<%= render 'post_moderation_actions', post: post if post %>
</article>

View File

@ -0,0 +1,3 @@
<% user_detail = user.thredded_user_detail %>
<%= user_detail.moderation_state.to_s.humanize %>
<%= time_ago user_detail.moderation_state_changed_at, default: '' %>

View File

@ -0,0 +1,12 @@
<% post, content = post_and_content if local_assigns.key?(:post_and_content) %>
<%= content_tag :article, id: dom_id(post), class: 'thredded--post thredded--post-moderation' do %>
<%= render 'thredded/posts_common/actions', post: post %>
<%= render 'thredded/posts_common/header_with_topic', post: post %>
<%= content || render('thredded/posts/content', post: post) %>
<% if post.blocked? %>
<p class="thredded--alert thredded--alert-danger">
<%= render 'thredded/shared/content_moderation_blocked_state', moderation_record: post.last_moderation_record %>
</p>
<% end %>
<%= render 'post_moderation_actions', post: post %>
<% end %>

View File

@ -0,0 +1,13 @@
<%= form_tag users_moderation_path,
method: 'get',
class: 'thredded--form thredded--navigation--search thredded--navigation--search--right',
'data-thredded-turboform' => true do %>
<%= label_tag :q, t('thredded.moderation.search_users.form_label') %>
<%= text_field_tag :q, @query,
type: 'search',
required: true,
# If there are no results the user will likely want to change the query, so auto-focus.
autofocus: @query.presence && !@users.presence,
placeholder: t('thredded.moderation.search_users.form_placeholder') %>
<button type="submit"><%= t 'thredded.search.form.btn_submit' %></button>
<% end %>

View File

@ -0,0 +1,20 @@
<% content_for :thredded_page_title, t('thredded.nav.moderation') %>
<% content_for :thredded_page_id, 'thredded--moderation-activity' %>
<%= render 'nav' %>
<%= thredded_page do %>
<section class="thredded--main-section">
<h1><%= t 'thredded.recent_activity' %></h1>
<% if @last_moderated_record %>
<div class="thredded--moderated-notice">
<%= render 'post_moderation_record', post_moderation_record: @last_moderated_record %>
</div>
<% end %>
<% if @posts.present? %>
<%= render_posts @posts,
partial: 'thredded/moderation/post',
content_partial: 'thredded/posts/content' %>
<%= paginate @posts %>
<% end %>
</section>
<% end %>

View File

@ -0,0 +1,13 @@
<% content_for :thredded_page_title,
safe_join([t('thredded.nav.moderation'), t('thredded.nav.moderation_history')], ': ') %>
<% content_for :thredded_page_id, 'thredded--moderation-history' %>
<%= render 'nav' %>
<%= thredded_page do %>
<section class="thredded--main-section">
<% if @post_moderation_records.present? %>
<%= render partial: 'post_moderation_record', collection: @post_moderation_records %>
<%= paginate @post_moderation_records %>
<% end %>
</section>
<% end %>

Some files were not shown because too many files have changed in this diff Show More