diff --git a/Gemfile b/Gemfile
index 82174f50..ef05fa73 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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
diff --git a/Gemfile.lock b/Gemfile.lock
index 8beb41e0..366e2872 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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
diff --git a/README.rdoc b/README.rdoc
index 33a2b43d..3b7c7f3c 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -1,12 +1,12 @@
-= notebook
+= Notebook.ai
{
}[https://travis-ci.org/indentlabs/notebook]
{}[https://codeclimate.com/github/indentlabs/notebook]
{
}[https://codeclimate.com/github/indentlabs/notebook/coverage]
{
}[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
diff --git a/app/assets/javascripts/contributors.coffee b/app/assets/javascripts/contributors.coffee
new file mode 100644
index 00000000..24f83d18
--- /dev/null
+++ b/app/assets/javascripts/contributors.coffee
@@ -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/
diff --git a/app/assets/stylesheets/documents.scss b/app/assets/stylesheets/contributors.scss
similarity index 62%
rename from app/assets/stylesheets/documents.scss
rename to app/assets/stylesheets/contributors.scss
index ec6aac28..d06e8cbb 100644
--- a/app/assets/stylesheets/documents.scss
+++ b/app/assets/stylesheets/contributors.scss
@@ -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/
diff --git a/app/assets/stylesheets/emails.scss b/app/assets/stylesheets/emails.scss
deleted file mode 100644
index 35db24d0..00000000
--- a/app/assets/stylesheets/emails.scss
+++ /dev/null
@@ -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/
diff --git a/app/assets/stylesheets/lab.scss b/app/assets/stylesheets/lab.scss
deleted file mode 100644
index 7288a665..00000000
--- a/app/assets/stylesheets/lab.scss
+++ /dev/null
@@ -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/
diff --git a/app/assets/stylesheets/navigator.css b/app/assets/stylesheets/navigator.css
deleted file mode 100644
index 4bfbeb09..00000000
--- a/app/assets/stylesheets/navigator.css
+++ /dev/null
@@ -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/
diff --git a/app/assets/stylesheets/voting.scss b/app/assets/stylesheets/voting.scss
deleted file mode 100644
index 17469c52..00000000
--- a/app/assets/stylesheets/voting.scss
+++ /dev/null
@@ -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/
diff --git a/app/authorizers/attribute_authorizer.rb b/app/authorizers/attribute_authorizer.rb
new file mode 100644
index 00000000..d427bcb8
--- /dev/null
+++ b/app/authorizers/attribute_authorizer.rb
@@ -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
diff --git a/app/authorizers/collective_content_authorizer.rb b/app/authorizers/collective_content_authorizer.rb
index fb616053..cd77b85e 100644
--- a/app/authorizers/collective_content_authorizer.rb
+++ b/app/authorizers/collective_content_authorizer.rb
@@ -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
diff --git a/app/authorizers/content_authorizer.rb b/app/authorizers/content_authorizer.rb
index 33f1abd7..c8d7f28d 100644
--- a/app/authorizers/content_authorizer.rb
+++ b/app/authorizers/content_authorizer.rb
@@ -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
diff --git a/app/authorizers/core_content_authorizer.rb b/app/authorizers/core_content_authorizer.rb
index 3a2459c9..0050fdf4 100644
--- a/app/authorizers/core_content_authorizer.rb
+++ b/app/authorizers/core_content_authorizer.rb
@@ -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
diff --git a/app/authorizers/extended_content_authorizer.rb b/app/authorizers/extended_content_authorizer.rb
index e0b8c990..28c0b781 100644
--- a/app/authorizers/extended_content_authorizer.rb
+++ b/app/authorizers/extended_content_authorizer.rb
@@ -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
diff --git a/app/authorizers/universe_core_content_authorizer.rb b/app/authorizers/universe_core_content_authorizer.rb
index 121cafac..793be96c 100644
--- a/app/authorizers/universe_core_content_authorizer.rb
+++ b/app/authorizers/universe_core_content_authorizer.rb
@@ -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
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index defebcdd..b3d66ccc 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -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
diff --git a/app/controllers/attribute_fields_controller.rb b/app/controllers/attribute_fields_controller.rb
index ae52124b..6e627385 100644
--- a/app/controllers/attribute_fields_controller.rb
+++ b/app/controllers/attribute_fields_controller.rb
@@ -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
diff --git a/app/controllers/concerns/has_ownership.rb b/app/controllers/concerns/has_ownership.rb
deleted file mode 100644
index aff96816..00000000
--- a/app/controllers/concerns/has_ownership.rb
+++ /dev/null
@@ -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
diff --git a/app/controllers/content_controller.rb b/app/controllers/content_controller.rb
index e4c22707..392d7761 100644
--- a/app/controllers/content_controller.rb
+++ b/app/controllers/content_controller.rb
@@ -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', {
diff --git a/app/controllers/contributors_controller.rb b/app/controllers/contributors_controller.rb
new file mode 100644
index 00000000..2e2ad2d6
--- /dev/null
+++ b/app/controllers/contributors_controller.rb
@@ -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
diff --git a/app/controllers/export_controller.rb b/app/controllers/export_controller.rb
index 7c44ead2..b3b2cc75 100644
--- a/app/controllers/export_controller.rb
+++ b/app/controllers/export_controller.rb
@@ -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"
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index e7df9fb4..8d24f4a5 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -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
diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb
index 4a5e1131..c972112d 100644
--- a/app/controllers/subscriptions_controller.rb
+++ b/app/controllers/subscriptions_controller.rb
@@ -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
diff --git a/app/controllers/universes_controller.rb b/app/controllers/universes_controller.rb
index 840a0f6a..b261041e 100644
--- a/app/controllers/universes_controller.rb
+++ b/app/controllers/universes_controller.rb
@@ -28,7 +28,7 @@ class UniversesController < ContentController
:history,
:privacy,
:notes, :private_notes,
- custom_attribute_values: [:name, :value]
+ custom_attribute_values: [:name, :value],
]
end
end
diff --git a/app/helpers/contributors_helper.rb b/app/helpers/contributors_helper.rb
new file mode 100644
index 00000000..2d0349f0
--- /dev/null
+++ b/app/helpers/contributors_helper.rb
@@ -0,0 +1,2 @@
+module ContributorsHelper
+end
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
new file mode 100644
index 00000000..d25d8892
--- /dev/null
+++ b/app/mailers/application_mailer.rb
@@ -0,0 +1,4 @@
+class ApplicationMailer < ActionMailer::Base
+ default from: "from@example.com"
+ layout 'mailer'
+end
diff --git a/app/mailers/collaboration_mailer.rb b/app/mailers/collaboration_mailer.rb
new file mode 100644
index 00000000..5bc1c99d
--- /dev/null
+++ b/app/mailers/collaboration_mailer.rb
@@ -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
diff --git a/app/models/attribute.rb b/app/models/attribute.rb
index af42c602..c3272935 100644
--- a/app/models/attribute.rb
+++ b/app/models/attribute.rb
@@ -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') }
diff --git a/app/models/attribute_category.rb b/app/models/attribute_category.rb
index 529c09c0..fa5e66f2 100644
--- a/app/models/attribute_category.rb
+++ b/app/models/attribute_category.rb
@@ -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
diff --git a/app/models/attribute_field.rb b/app/models/attribute_field.rb
index 268470a0..880b91bf 100644
--- a/app/models/attribute_field.rb
+++ b/app/models/attribute_field.rb
@@ -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
diff --git a/app/models/billing_plan.rb b/app/models/billing_plan.rb
index ad28a549..84d59b53 100644
--- a/app/models/billing_plan.rb
+++ b/app/models/billing_plan.rb
@@ -1,2 +1,3 @@
class BillingPlan < ActiveRecord::Base
+ PREMIUM_IDS = [2, 3, 4, 5, 6]
end
diff --git a/app/models/concerns/has_changelog.rb b/app/models/concerns/has_changelog.rb
index 684918d8..ea4575e7 100644
--- a/app/models/concerns/has_changelog.rb
+++ b/app/models/concerns/has_changelog.rb
@@ -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
diff --git a/app/models/concerns/has_content_groupers.rb b/app/models/concerns/has_content_groupers.rb
index 862dc6c4..d5b18521 100644
--- a/app/models/concerns/has_content_groupers.rb
+++ b/app/models/concerns/has_content_groupers.rb
@@ -22,7 +22,7 @@ module HasContentGroupers
} # }
# Siblingships
- has_many connecting_class_name
+ has_many connecting_class_name, dependent: :destroy
# Siblings
has_many relation,
diff --git a/app/models/content_groupers/childrenship.rb b/app/models/content_groupers/childrenship.rb
index 4dec0b84..48bf7cd9 100644
--- a/app/models/content_groupers/childrenship.rb
+++ b/app/models/content_groupers/childrenship.rb
@@ -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
diff --git a/app/models/content_groupers/siblingship.rb b/app/models/content_groupers/siblingship.rb
index aa269980..3133b944 100644
--- a/app/models/content_groupers/siblingship.rb
+++ b/app/models/content_groupers/siblingship.rb
@@ -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
diff --git a/app/models/content_types/universe.rb b/app/models/content_types/universe.rb
index a5d40178..161e2e11 100644
--- a/app/models/content_types/universe.rb
+++ b/app/models/content_types/universe.rb
@@ -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
diff --git a/app/models/contributor.rb b/app/models/contributor.rb
new file mode 100644
index 00000000..0d023851
--- /dev/null
+++ b/app/models/contributor.rb
@@ -0,0 +1,4 @@
+class Contributor < ActiveRecord::Base
+ belongs_to :universe
+ belongs_to :user
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 3f620d55..8d260c26 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
diff --git a/app/services/contributor_service.rb b/app/services/contributor_service.rb
new file mode 100644
index 00000000..bc9b1f28
--- /dev/null
+++ b/app/services/contributor_service.rb
@@ -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
\ No newline at end of file
diff --git a/app/services/permission_service.rb b/app/services/permission_service.rb
new file mode 100644
index 00000000..59ad64ea
--- /dev/null
+++ b/app/services/permission_service.rb
@@ -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
diff --git a/app/views/cards/intros/_content_type_intro.html.erb b/app/views/cards/intros/_content_type_intro.html.erb
index dd750909..2fb004d2 100644
--- a/app/views/cards/intros/_content_type_intro.html.erb
+++ b/app/views/cards/intros/_content_type_intro.html.erb
@@ -5,7 +5,7 @@
+ <%= @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. +
++ 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. +
+ ++ Happy worldbuilding! +
+ ++ 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. +
+ + diff --git a/app/views/collaboration_mailer/contributor_invitation.text.erb b/app/views/collaboration_mailer/contributor_invitation.text.erb new file mode 100644 index 00000000..10df67c9 --- /dev/null +++ b/app/views/collaboration_mailer/contributor_invitation.text.erb @@ -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. \ No newline at end of file diff --git a/app/views/content/contexts/_universe.html.erb b/app/views/content/contexts/_universe.html.erb index 5509f05e..1741e22b 100644 --- a/app/views/content/contexts/_universe.html.erb +++ b/app/views/content/contexts/_universe.html.erb @@ -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? %> diff --git a/app/views/content/display/_changelog.html.erb b/app/views/content/display/_changelog.html.erb index 75e0d92f..3ddb4392 100644 --- a/app/views/content/display/_changelog.html.erb +++ b/app/views/content/display/_changelog.html.erb @@ -3,6 +3,7 @@+ Invited <%= time_ago_in_words contributor.created_at %> ago +
+ <% if current_user && content.user == current_user %> ++ <%= 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." } %> +
+ <% end %> + group ++ Universes can have an unlimited number of collaborators. +
++ When a user is added as a collaborator to a universe, they are allowed to: + +
+ They are NOT allowed to: + +
+ 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 %> +
+ + <%= render partial: 'content/display/contributors', locals: { content: content } %> +<% end %> + +<% if user_signed_in? && content.user == current_user %> ++ You may also add additional contributors to this universe by inviting them by email below. +
+ ++ 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. +
++ <% 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." } %> +
+<% end %> \ No newline at end of file diff --git a/app/views/content/index.html.erb b/app/views/content/index.html.erb index 7797f889..3640273f 100644 --- a/app/views/content/index.html.erb +++ b/app/views/content/index.html.erb @@ -1,6 +1,5 @@ <% - content_type_class = @content.build.class - content_type = content_type_class.name.downcase + content_type = @content_type_class.name.downcase %> <% content_for :sidebar_top do %> @@ -32,28 +31,28 @@ <% if @content.any? %><%= t("content_descriptions.#{content_type}") %>
- <% 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 %><%= content.description %>
- <% if current_user && current_user == content.user %> + <% if user_signed_in? && content.updatable_by?(current_user) %>