mirror of
https://github.com/indentlabs/notebook.git
synced 2025-10-26 11:19:22 +00:00
Merge branch 'master' into omniauth_for_social_sites
This commit is contained in:
commit
edf2f4786a
14
Gemfile
14
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
|
||||
|
||||
127
Gemfile.lock
127
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
|
||||
|
||||
23
README.rdoc
23
README.rdoc
@ -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
|
||||
|
||||
3
app/assets/javascripts/contributors.coffee
Normal file
3
app/assets/javascripts/contributors.coffee
Normal 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/
|
||||
@ -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/
|
||||
@ -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/
|
||||
@ -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/
|
||||
@ -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/
|
||||
@ -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/
|
||||
29
app/authorizers/attribute_authorizer.rb
Normal file
29
app/authorizers/attribute_authorizer.rb
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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', {
|
||||
|
||||
16
app/controllers/contributors_controller.rb
Normal file
16
app/controllers/contributors_controller.rb
Normal 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
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -28,7 +28,7 @@ class UniversesController < ContentController
|
||||
:history,
|
||||
:privacy,
|
||||
:notes, :private_notes,
|
||||
custom_attribute_values: [:name, :value]
|
||||
custom_attribute_values: [:name, :value],
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
2
app/helpers/contributors_helper.rb
Normal file
2
app/helpers/contributors_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module ContributorsHelper
|
||||
end
|
||||
4
app/mailers/application_mailer.rb
Normal file
4
app/mailers/application_mailer.rb
Normal file
@ -0,0 +1,4 @@
|
||||
class ApplicationMailer < ActionMailer::Base
|
||||
default from: "from@example.com"
|
||||
layout 'mailer'
|
||||
end
|
||||
13
app/mailers/collaboration_mailer.rb
Normal file
13
app/mailers/collaboration_mailer.rb
Normal 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
|
||||
@ -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') }
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
class BillingPlan < ActiveRecord::Base
|
||||
PREMIUM_IDS = [2, 3, 4, 5, 6]
|
||||
end
|
||||
|
||||
@ -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
|
||||
|
||||
@ -22,7 +22,7 @@ module HasContentGroupers
|
||||
} # }
|
||||
|
||||
# Siblingships
|
||||
has_many connecting_class_name
|
||||
has_many connecting_class_name, dependent: :destroy
|
||||
|
||||
# Siblings
|
||||
has_many relation,
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
4
app/models/contributor.rb
Normal file
4
app/models/contributor.rb
Normal file
@ -0,0 +1,4 @@
|
||||
class Contributor < ActiveRecord::Base
|
||||
belongs_to :universe
|
||||
belongs_to :user
|
||||
end
|
||||
@ -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
|
||||
|
||||
39
app/services/contributor_service.rb
Normal file
39
app/services/contributor_service.rb
Normal 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
|
||||
66
app/services/permission_service.rb
Normal file
66
app/services/permission_service.rb
Normal 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
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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.
|
||||
@ -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? %>
|
||||
|
||||
@ -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>
|
||||
|
||||
37
app/views/content/display/_contributors.html.erb
Normal file
37
app/views/content/display/_contributors.html.erb
Normal 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>
|
||||
@ -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;',
|
||||
|
||||
@ -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 %>
|
||||
|
||||
53
app/views/content/form/_contributors.html.erb
Normal file
53
app/views/content/form/_contributors.html.erb
Normal 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 %>
|
||||
@ -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') +
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
21
app/views/content/form/contributors/_add.html.erb
Normal file
21
app/views/content/form/contributors/_add.html.erb
Normal 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>
|
||||
15
app/views/content/form/contributors/_leave.html.erb
Normal file
15
app/views/content/form/contributors/_leave.html.erb
Normal 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 %>
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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 %>
|
||||
|
||||
|
||||
@ -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 %>
|
||||
|
||||
2
app/views/contributors/create.html.erb
Normal file
2
app/views/contributors/create.html.erb
Normal file
@ -0,0 +1,2 @@
|
||||
<h1>Contributors#create</h1>
|
||||
<p>Find me in app/views/contributors/create.html.erb</p>
|
||||
2
app/views/contributors/destroy.html.erb
Normal file
2
app/views/contributors/destroy.html.erb
Normal file
@ -0,0 +1,2 @@
|
||||
<h1>Contributors#destroy</h1>
|
||||
<p>Find me in app/views/contributors/destroy.html.erb</p>
|
||||
@ -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>
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
<h1>Emails#one_click_unsubscribe</h1>
|
||||
<p>Find me in app/views/emails/one_click_unsubscribe.html.erb</p>
|
||||
@ -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>
|
||||
|
||||
@ -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 %>
|
||||
|
||||
@ -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"> </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"> </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 %>
|
||||
|
||||
@ -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 %>
|
||||
|
||||
@ -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>
|
||||
|
||||
23
app/views/layouts/forum.html.erb
Normal file
23
app/views/layouts/forum.html.erb
Normal 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>
|
||||
5
app/views/layouts/mailer.html.erb
Normal file
5
app/views/layouts/mailer.html.erb
Normal file
@ -0,0 +1,5 @@
|
||||
<html>
|
||||
<body>
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
1
app/views/layouts/mailer.text.erb
Normal file
1
app/views/layouts/mailer.text.erb
Normal file
@ -0,0 +1 @@
|
||||
<%= yield %>
|
||||
@ -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 } %>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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;">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
1
app/views/thredded/categories/_category.html.erb
Normal file
1
app/views/thredded/categories/_category.html.erb
Normal file
@ -0,0 +1 @@
|
||||
<li class="<%= category.name.underscore %>"><%= category.name %></li>
|
||||
6
app/views/thredded/error_pages/forbidden.html.erb
Normal file
6
app/views/thredded/error_pages/forbidden.html.erb
Normal 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 %>
|
||||
6
app/views/thredded/error_pages/not_found.html.erb
Normal file
6
app/views/thredded/error_pages/not_found.html.erb
Normal 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 %>
|
||||
11
app/views/thredded/kaminari/_first_page.html.erb
Normal file
11
app/views/thredded/kaminari/_first_page.html.erb
Normal 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>
|
||||
8
app/views/thredded/kaminari/_gap.html.erb
Normal file
8
app/views/thredded/kaminari/_gap.html.erb
Normal 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>
|
||||
11
app/views/thredded/kaminari/_last_page.html.erb
Normal file
11
app/views/thredded/kaminari/_last_page.html.erb
Normal 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>
|
||||
11
app/views/thredded/kaminari/_next_page.html.erb
Normal file
11
app/views/thredded/kaminari/_next_page.html.erb
Normal 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>
|
||||
12
app/views/thredded/kaminari/_page.html.erb
Normal file
12
app/views/thredded/kaminari/_page.html.erb
Normal 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>
|
||||
23
app/views/thredded/kaminari/_paginator.html.erb
Normal file
23
app/views/thredded/kaminari/_paginator.html.erb
Normal 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 -%>
|
||||
11
app/views/thredded/kaminari/_prev_page.html.erb
Normal file
11
app/views/thredded/kaminari/_prev_page.html.erb
Normal 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>
|
||||
28
app/views/thredded/messageboard_groups/new.html.erb
Normal file
28
app/views/thredded/messageboard_groups/new.html.erb
Normal 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 %>
|
||||
24
app/views/thredded/messageboards/_form.html.erb
Normal file
24
app/views/thredded/messageboards/_form.html.erb
Normal 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 %>
|
||||
24
app/views/thredded/messageboards/_messageboard.html.erb
Normal file
24
app/views/thredded/messageboards/_messageboard.html.erb
Normal 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 %>
|
||||
15
app/views/thredded/messageboards/edit.html.erb
Normal file
15
app/views/thredded/messageboards/edit.html.erb
Normal 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 %>
|
||||
34
app/views/thredded/messageboards/index.html.erb
Normal file
34
app/views/thredded/messageboards/index.html.erb
Normal 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 %>
|
||||
15
app/views/thredded/messageboards/new.html.erb
Normal file
15
app/views/thredded/messageboards/new.html.erb
Normal 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 %>
|
||||
19
app/views/thredded/moderation/_nav.html.erb
Normal file
19
app/views/thredded/moderation/_nav.html.erb
Normal 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 %>
|
||||
19
app/views/thredded/moderation/_post.html.erb
Normal file
19
app/views/thredded/moderation/_post.html.erb
Normal 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 %>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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: '' %>
|
||||
12
app/views/thredded/moderation/_user_post.html.erb
Normal file
12
app/views/thredded/moderation/_user_post.html.erb
Normal 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 %>
|
||||
13
app/views/thredded/moderation/_users_search_form.html.erb
Normal file
13
app/views/thredded/moderation/_users_search_form.html.erb
Normal 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 %>
|
||||
20
app/views/thredded/moderation/activity.html.erb
Normal file
20
app/views/thredded/moderation/activity.html.erb
Normal 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 %>
|
||||
13
app/views/thredded/moderation/history.html.erb
Normal file
13
app/views/thredded/moderation/history.html.erb
Normal 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
Loading…
Reference in New Issue
Block a user