Add additional content boxes under posts (#86)

* Add additional content boxes under posts

* Fix issue with classic article for under content
This commit is contained in:
Ben Halpern 2018-03-16 16:35:54 -04:00 committed by GitHub
parent 4d04f7f852
commit c1e623ebc2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 442 additions and 46 deletions

View file

@ -38,6 +38,7 @@ function callInitalizers(){
initializeAnalytics();
initializeCommentDropdown();
initializeSettings();
initializeAdditionalContentBoxes();
if (!initializeLiveArticle.called){
initializeLiveArticle();
}

View file

@ -0,0 +1,19 @@
function initializeAdditionalContentBoxes() {
var el = document.getElementById("additional-content-area");
if (el) {
window.fetch('/additional_content_boxes?article_id='+el.dataset.articleId, {
method: 'GET',
credentials: 'same-origin'
}).then(function (response) {
if (response.status === 200) {
response.text().then(function(html){
el.innerHTML = html;
initializeReadingListIcons();
initializeAllFollowButts();
})
} else {
// there's currently no errorCb.
}
});
}
}

View file

@ -1,6 +1,16 @@
function initializeSponsorshipVisibility() {
var el = document.getElementById("sponsorship-widget");
var el = document.getElementById("sponsorship-widget") || document.getElementById("partner-content-display");
var user = userData();
if (el) {
setTimeout(function(){
if (window.ga) {
if (document.querySelectorAll('[data-partner-seen]').length === 0) {
ga('send', 'event', 'view', 'sponsor displayed on page', el.dataset.details, null);
el.dataset.partnerSeen = "true";
}
}
}, 400)
}
if (el && user && user.display_sponsors){
el.classList.add("showing");
setTimeout(function(){

View file

@ -78,3 +78,165 @@
}
}
}
.show-page-content-display {
vertical-align:top;
font-family: $helvetica;
margin:40px auto;
position: relative;
overflow:hidden;
a{
color: $black;
}
.content-classification{
margin: 30px 20px 0px;
font-weight: bold;
text-align: left;
font-size:0.78em;
.content-classification-text{
background: lighten($yellow, 12%);
display: inline-block;
padding: 6px 25px;
}
}
.main-content-display{
padding:20px 0px;
width: calc(90% - 60px);
padding: 30px;
margin-bottom: 60px;
@media screen and ( min-width: 680px ){
float: left;
width: calc(97% - 365px);
border-right: 2px solid $light-medium-gray;
margin-left: 3%;
margin-top:10px;
padding: 15px 45px 35px 15px;
}
h2{
margin:0px auto 10px;
font-size:2.2em;
}
.content-author{
img{
border-radius: 100%;
width: 28px;
vertical-align: -7px;
margin-right: 3px;
}
a{
color: #666666;
}
}
p {
margin-bottom: 30px;
}
.cta{
padding: 7px 20px;
font-size: 1.2em;
margin-top: 10px;
margin-right: 3px;
display: inline-block;
}
.engagement-count{
font-family: $helvetica-condensed;
color: $medium-gray;
margin-top: -18px;
margin-bottom: -25px;
img{
height: 20px;
min-width: 27px;
vertical-align: -3px;
margin-right: 3px;
&.comments-bubble{
margin-left: 10px;
}
}
}
.bookmark-engage{
font-family: $helvetica-condensed;
background: darken($purple, 26%);
color: white;
letter-spacing: 1px;
border: 0px;
font-size: 1.2em;
padding: 7px 16px;
margin-top: 10px;
.bm-success{
display: none;
}
&.selected{
background: white;
color: darken($purple, 26%);
.bm-success{
display: inline-block;
img{
width: 18px;
vertical-align: -2px;
}
}
.bm-initial{
display: none;
}
}
}
}
.secondary-content-display{
display: none;
@media screen and ( min-width: 680px ){
display: block;
}
float: left;
width: 300px;
text-align:center;
padding:20px 0px;
.profile-pic-wrapper{
width: 110px;
height: 110px;
border-radius: 100%;
margin: auto;
&.wide-profile-image-wrapper{
height: 50px;
width: 150px;
margin-top: 50px;
margin-bottom: 10px;
}
img{
vertical-align: middle;
&.profile-image{
width: calc(100% - 8px);
height: calc(100% - 8px);
border-radius: 100%;
}
&.wide-image{
width: calc(100% - 8px);
}
}
}
.org-name{
font-weight: bold;
margin: 10px auto;
font-size: 1.1em;
}
.follow-action-button{
background: $green;
color: $black;
border: 0;
font-size: 1em;
border: 0;
padding: 3px 6px;
border-radius: 3px;
width:170px;
}
.org-summary{
font-size:0.88em;
width: 200px;
margin: 20px auto;
text-align:left;
a{
color: $bold-blue;
}
}
}
}

View file

@ -0,0 +1,16 @@
class AdditionalContentBoxesController < ApplicationController
def index
articles_ids = params[:article_id].split(",")
@article = Article.find(articles_ids[0])
@for_user_article = ClassicArticle.
new(current_user || @article, {not_ids: articles_ids}).get
if (!user_signed_in? || current_user&.display_sponsors) && @article.user.permit_adjacent_sponsors
@boosted_article = BoostedArticle.
new(current_user, @article, {not_ids: (articles_ids+[@for_user_article])}).get
end
@alt_classic = ClassicArticle.
new(@article, {not_ids: (articles_ids+[@for_user_article])}).get unless @boosted_article
render "boxes", layout: false
end
end

View file

@ -53,6 +53,22 @@ class Internal::ArticlesController < Internal::ApplicationController
page(params[:page]).
limited_columns_internal_select.
per(50)
when "classic-candidate"
@articles = Article.where("positive_reactions_count > ?", ClassicArticle.new.minimum_reaction_count).
includes(:user).
order("positive_reactions_count DESC").
where(featured: false).
page(params[:page]).
per(100).
limited_columns_internal_select
when "classic"
@articles = Article.where("positive_reactions_count > ?", ClassicArticle.new.minimum_reaction_count).
includes(:user).
order("positive_reactions_count DESC").
where(featured: true).
page(params[:page]).
per(100).
limited_columns_internal_select
else #MIX
@articles = Article.
where(published: true).
@ -96,6 +112,7 @@ class Internal::ArticlesController < Internal::ApplicationController
def article_params
params.require(:article).permit(:featured,
:social_image,
:body_markdown,
:approved,
:live_now,
:main_image_background_hex_color,

View file

@ -43,6 +43,7 @@ class OrganizationsController < ApplicationController
def organization_params
params.require(:organization).permit(:name,
:summary,
:tag_line,
:slug,
:url,
:proof,

View file

@ -224,6 +224,7 @@ class StoriesController < ApplicationController
assign_second_and_third_user
not_found if permission_denied?
set_surrogate_key_header @article.record_key
@classic_article = ClassicArticle.new(@article).get
unless user_signed_in?
response.headers["Surrogate-Control"] = "max-age=10000, stale-while-revalidate=30, stale-if-error=86400"
end

View file

@ -182,6 +182,7 @@ class UsersController < ApplicationController
prefer_language_fr
prefer_language_it
display_sponsors
permit_adjacent_sponsors
feed_admin_publish_permission]
accessible << %i[password password_confirmation] unless params[:user][:password].blank?
params.require(:user).

View file

@ -0,0 +1,18 @@
class BoostedArticle
attr_accessor :user, :article, :tags, :not_ids
def initialize(user, article, options)
@user = user
@article = article
@tags = (user&.cached_followed_tag_names.to_a + article.decorate.cached_tag_list_array).compact
@not_ids = options[:not_ids]
end
def get
Article.where(boosted: true).
includes(:user).
includes(:organization).
where.not(id: not_ids, organization_id: nil).
tagged_with(tags, any: true).sample
end
end

View file

@ -1,35 +1,53 @@
class ClassicArticle
attr_accessor :user
def initialize(user)
@user = user
attr_accessor :input, :not_ids
def initialize(input=nil, options={})
@input = input
@not_ids = options[:not_ids]
end
def get
possible_articles = []
5.times do
possible_articles << cached_qualifying_article
if rand(5) == 1
random_high_quality_article
else
qualifying_articles(random_supported_tag_names).where.not(id: not_ids).compact.sample ||
random_high_quality_article
end
possible_articles.compact.sample
end
def cached_qualifying_article
Rails.cache.fetch("classic-article-for-tag-#{random_supported_tag_name}_#{rand(0..1)}", expires_in: 45.minutes) do
Article.tagged_with(random_supported_tag_name).
def qualifying_articles(tag_names)
tag_name = tag_names.sample
Rails.cache.fetch("classic-article-for-tag-#{tag_name}}", expires_in: 45.minutes) do
articles = Article.tagged_with(tag_name).
includes(:user).
where(published: true, featured: true).
where("positive_reactions_count > ?", minimum_reaction_count).
where("published_at > ?", 10.months.ago).
order("RANDOM()").
first
order("RANDOM()")
end
end
def random_supported_tag_name
user.decorate.cached_followed_tags.where(supported: true).where.not(name: "ama").sample&.name
def random_high_quality_article
Article.where(published: true, featured: true).
where("positive_reactions_count > ?", 75).
includes(:user).
order("RANDOM()").
where.not(id: not_ids).
first
end
def random_supported_tag_names
if input.class.name == "User"
input.decorate.cached_followed_tags.where(supported: true).where.not(name: "ama").pluck(:name)
elsif input.class.name == "Article"
Tag.where(supported: true, name: input.decorate.cached_tag_list_array).where.not(name: "ama").pluck(:name)
else
["discuss"]
end
end
def minimum_reaction_count
if Rails.env.production?
36
45
else
1
end

View file

@ -67,7 +67,7 @@ class Article < ApplicationRecord
:video, :user_id, :organization_id, :video_source_url, :video_code,
:video_thumbnail_url, :video_closed_caption_track_url, :social_image,
:published_from_feed, :crossposted_at, :published_at, :featured_number,
:live_now, :last_buffered, :facebook_last_buffered, :created_at
:live_now, :last_buffered, :facebook_last_buffered, :created_at, :body_markdown
)
}
@ -329,7 +329,7 @@ class Article < ApplicationRecord
end
def evaluate_front_matter(front_matter)
token_msg = "From the DEV community. Sharing ideas that makes us all better developers."
token_msg = body_text[0..80] + "..."
self.title = front_matter["title"] if front_matter["title"].present?
if front_matter["tags"].present?
ActsAsTaggableOn::Taggable::Cache.included(Article)
@ -402,7 +402,7 @@ class Article < ApplicationRecord
end
def set_last_comment_at
self.last_comment_at = Time.now if published && last_comment_at < 30.days.ago
self.last_comment_at = published_at.present? if published_at.present? && last_comment_at.blank?
end
def title_to_slug

View file

@ -14,6 +14,8 @@ class Organization < ApplicationRecord
length: { maximum: 50 }
validates :summary,
length: { maximum: 250 }
validates :tag_line,
length: { maximum: 60 }
validates :jobs_email, email: true, allow_blank: true
validates :text_color_hex, format: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/
validates :bg_color_hex, format: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/

View file

@ -0,0 +1,13 @@
<div class="container show-page-content-display"
data-details="<%= article.organization&.slug %>__<%= article.slug %>"
id="<%= article.organization ? "partner-content-display" : "classic_article_#{article.id}" %>">
<div class="content-classification">
<span class="content-classification-text"><%= classification %></span>
</div>
<div class="main-content-display">
<%= render "additional_content_boxes/article_content_area", article: article, organization: article.organization %>
</div>
<div class="secondary-content-display">
<%= render "additional_content_boxes/article_followable_area", followable: article.organization || article.user, follow_cue: follow_cue %>
</div>
</div>

View file

@ -0,0 +1,30 @@
<h2><a href="<%= article.path %>" class="<%= 'partner-link' if organization %>" data-details="<%= organization&.slug %>__<%= article.slug %>"><%= article.title %></a></h2>
<div class="content-author">
<a href="<%= article.user.path %>">
<img class="profile-pic" src="<%= ProfileImage.new(article.user).get(50)%>" alt="<%= article.user.username %> profile image"/>
<span><%= article.user.name %></span>
</a>
</div>
<p>
<a href="<%= article.path %>" class="<%= 'partner-link' if organization %>" data-details="<%= organization&.slug %>__<%= article.slug %>">
<%= article.description %>
</a>
<div class="engagement-count">
<% if article.positive_reactions_count > 0 %>
<img src="<%= asset_path("reactions-stack.png") %>" alt="Reactions" /> <%= article.positive_reactions_count %>
<% end %>
<% if article.comments_count > 0 %>
<img src="<%= asset_path("comments-bubble.png") %>" alt="Reactions" class="comments-bubble"/> <%= article.comments_count %>
<% end %>
</div>
</p>
<a href="<%= article.path %>" class="cta<%= ' partner-link' if organization %>" data-details="<%= organization&.slug %>__<%= article.slug %>">READ POST</a>
<button
class="article-engagement-count bookmark-engage<%= ' partner-link' if organization %>"
data-reactable-id="<%= article.id %>"
aria-label="Save to reading list"
data-details="<%= organization&.slug %>__<%= article.slug %>"
title="Save to reading list">
<span class="bm-success"><img src="<%= asset_path("readinglist-button.png") %>" alt="bookmark" /> SAVED</span>
<span class="bm-initial">SAVE FOR LATER</span>
</button>

View file

@ -0,0 +1,24 @@
<% if followable.class.name == "Organization" && followable.nav_image.present? %>
<div class="profile-pic-wrapper wide-profile-image-wrapper">
<a href="<%= followable.path %>" />
<img class="wide-image" src="<%= CloudinaryHelper.cl_image_path(followable.nav_image_url,
type: "fetch",
crop: "fill",
width: 220,
quality: "auto",
flags: "progressive",
fetch_format: "auto",
sign_url: true) %>" />
</a>
</div>
<% else %>
<div class="profile-pic-wrapper">
<a href="<%= followable.path %>" />
<img class="profile-image" src="<%= ProfileImage.new(followable).get(200) %>" style="border: 4px solid <%= followable.bg_color_hex %>" />
</div>
<div class="org-name">
<a href="<%= followable.path %>" /><%= followable.name %></a>
</div>
<% end %>
<button class="cta follow-action-button user-profile-follow-button" style="color:<%= user_colors(followable)[:text] %>;background-color:<%=user_colors(followable)[:bg] %>" data-info='{"id":<%= followable.id%>,"className":"<%= followable.class.name %>"}'>&nbsp;</button>
<div class="org-summary"><%= follow_cue&.html_safe || followable.summary %></div>

View file

@ -0,0 +1,17 @@
<% if @boosted_article %>
<%= render "additional_content_boxes/article_box",
article: @boosted_article,
classification: "From one of our Community Sponsors",
follow_cue: @boosted_article.organization.tag_line || @boosted_article.organization.summary %>
<% elsif @alt_classic %>
<%= render "additional_content_boxes/article_box",
article: @alt_classic,
classification: "Classic Post from #{@alt_classic.readable_publish_date}",
follow_cue: "Follow <a href='#{@alt_classic.user.path}'>@#{@alt_classic.user.username}</a> to see more of their posts in your feed." %>
<% end %>
<% if @for_user_article %>
<%= render "additional_content_boxes/article_box",
article: @for_user_article,
classification: "One More You Might Like",
follow_cue: "Follow <a href='#{@for_user_article.user.path}'>@#{@for_user_article.user.username}</a> to see more of their posts in your feed." %>
<% end %>

View file

@ -180,7 +180,14 @@
</div>
<% end %>
</div>
<% cache("article-bottom-content-#{@article.id}", :expires_in => 12.hours) do %>
<% cache("article-bottom-content-#{@article.cached_tag_list_array.sort}", :expires_in => 6.hours) do %>
<% if @classic_article %>
<%= render "additional_content_boxes/article_box",
article: @classic_article,
classification: "Classic Post from #{@classic_article.readable_publish_date}",
follow_cue: "Follow <a href='#{@classic_article.user.path}'>@#{@classic_article.user.username}</a> to see more of their posts in your feed." %>
<% end %>
<div id="additional-content-area" data-article-id="<%= @article.id %>,<%= @classic_article&.id %>"></div>
<%= render 'articles/bottom_content' %>
<% end %>
</div>

View file

@ -39,31 +39,40 @@
&nbsp Live Now: <input name="article[live_now]" type="checkbox" <%= "checked" if article.live_now %>>
&nbsp Image BG color: <input name="article[main_image_background_hex_color]" value="<%= article.main_image_background_hex_color %>" id="featured_number_<%= article.id %>"><br/><br/>
Social Image: <input name="article[social_image]" value="<%= article.social_image %>" id="featured_number_<%= article.id %>">
<% if params[:state]&.include?("classic") %>
<br/><br>
<textarea cols="70" rows="6" wrap="hard" name="article[body_markdown]" /><%= article.body_markdown %></textarea>
<% end %>
&nbsp &nbsp <button class="btn btn-primary">SUBMIT</button>
</form>
</div>
<div class="inner-row" style="padding:10px;margin-top:20px;font-weight:800;">
<%= form_tag "/internal/buffer_updates" do %>
<input type="hidden" name="social_channel" value="twitter" />
<input type="hidden" name="article_id" value="<%= article.id %>" />
<p> Twitter </p>
<textarea cols="37" rows="6" wrap="hard" name="tweet" maxlength="230" /><%= article.title %></textarea>
<% if article.last_buffered %>
<button class="btn btn-danger" style="font-size:1em;">🙈 TWEETED <%= time_ago_in_words(article.last_buffered) %> ago</button>
<% else %>
<button class="btn btn-info" style="font-size:1em;">😎 TWEET 🚀🚀🚀🚀🚀🚀</button>
<% unless params[:state]&.include?("classic") %>
<div class="inner-row" style="padding:10px;margin-top:20px;font-weight:800;">
<%= form_tag "/internal/buffer_updates" do %>
<input type="hidden" name="social_channel" value="twitter" />
<input type="hidden" name="article_id" value="<%= article.id %>" />
<p> Twitter </p>
<textarea cols="37" rows="6" wrap="hard" name="tweet" maxlength="230" /><%= article.title %></textarea>
<% if article.last_buffered %>
<button class="btn btn-danger" style="font-size:1em;">🙈 TWEETED <%= time_ago_in_words(article.last_buffered) %> ago</button>
<% else %>
<button class="btn btn-info" style="font-size:1em;">😎 TWEET 🚀🚀🚀🚀🚀🚀</button>
<% end %>
<% end %>
<% end %>
<%= form_tag "/internal/buffer_updates" do %>
<input type="hidden" name="social_channel" value="facebook" />
<input type="hidden" name="article_id" value="<%= article.id %>" />
<p> Facebook & LinkedIn </p>
<textarea cols="37" rows="6" wrap="hard" name="fb_post" required/></textarea>
<% if article.facebook_last_buffered %>
<button class="btn btn-danger" style="font-size:1em;">🙈 POSTED <%= time_ago_in_words(article.facebook_last_buffered) %> ago</button>
<% else %>
<button class="btn btn-info" style="font-size:1em;">😎 POST 🚀🚀🚀🚀🚀🚀</button>
<%= form_tag "/internal/buffer_updates" do %>
<input type="hidden" name="social_channel" value="facebook" />
<input type="hidden" name="article_id" value="<%= article.id %>" />
<p> Facebook & LinkedIn </p>
<textarea cols="37" rows="6" wrap="hard" name="fb_post" required/></textarea>
<% if article.facebook_last_buffered %>
<button class="btn btn-danger" style="font-size:1em;">🙈 POSTED <%= time_ago_in_words(article.facebook_last_buffered) %> ago</button>
<% else %>
<button class="btn btn-info" style="font-size:1em;">😎 POST 🚀🚀🚀🚀🚀🚀</button>
<% end %>
<% end %>
<% end %>
</div>
</div>
<% end %>
</div>

View file

@ -27,6 +27,8 @@
<a href="/internal/articles?state=top-3" style="padding:10px;<%= "background:#89ffba" if params[:state] == "top-3" %>">Top (3 mo)</a> |
<a href="/internal/articles?state=top-6" style="padding:10px;<%= "background:#89ffba" if params[:state] == "top-6" %>">Top (6 mo)</a> |
<a href="/internal/articles?state=spam" style="padding:10px;<%= "background:#89ffba" if params[:state] == "spam" %>">Spam?</a>
<a href="/internal/articles?state=classic" style="padding:10px;<%= "background:#89ffba" if params[:state] == "classic" %>">Classic</a>
<a href="/internal/articles?state=classic-candidate" style="padding:10px;<%= "background:#89ffba" if params[:state] == "classic-candidate" %>">Classic Candidate</a>
</h3>
<br/>
<% if params[:state] && params[:state].include?("top-") && params[:state] != "top-3" && params[:state] != "top-6" %>
@ -44,7 +46,7 @@
<div class="row" style="font-size:1.5em;border:0px">
<h1>Newest Articles</h1>
<h1>All Articles</h1>
<%= paginate @articles %>
</div>

View file

@ -52,14 +52,18 @@
<h2>Sponsors</h2>
<h4 style="font-weight:400">
You have the option to remove sponsor messaging (where it is practical to do so). Our wonderful sponsors help sustain the platform and improve your experience, and we strive to make their presence unobtrusive, but feel free to use this setting if you wish.
You have the option to remove sponsor messaging (where it is practical to do so). Our wonderful sponsors help sustain the platform and improve your experience, and we strive to make their presence constructive to the community, but feel free to use this setting if you wish.
</h4>
<%= form_for(@user) do |f| %>
<div class="checkbox-field">
<div class="sub-field">
<%= f.check_box :display_sponsors %>
<%= f.label :display_sponsors, "Display Sponsors" %>
<%= f.label :display_sponsors, "Display Sponsors (When browsing)" %>
</div>
<div class="sub-field">
<%= f.check_box :permit_adjacent_sponsors %>
<%= f.label :permit_adjacent_sponsors, "Permit Nearby Sponsors (When publishing)" %>
</div>
</div>
<div class="field">

View file

@ -91,6 +91,10 @@
<%= f.label :tech_stack, "Our Stack" %>
<%= f.text_area :tech_stack, maxlength: 200 %>
</div>
<div class="field">
<%= f.label :tag_line %>
<%= f.text_area :tag_line, maxlegth: 60 %>
</div>
<div class="field">
<%= f.label :summary %>
<%= f.text_area :summary, maxlegth: 250 %>

View file

@ -10,6 +10,7 @@
"users",
"settings",
"about",
"additional_content_boxes",
"features",
"privacy",
"terms",

View file

@ -76,6 +76,7 @@ Rails.application.routes.draw do
resources :github_repos, only: [:create, :update]
resources :buffered_articles, only: [:index]
resources :events, only: [:index, :show]
resources :additional_content_boxes, only: [:index]
get "/notifications/:username" => "notifications#index"
patch "/onboarding_update" => "users#onboarding_update"
# resources :users

View file

@ -0,0 +1,5 @@
class AddBoostedToArticles < ActiveRecord::Migration[5.1]
def change
add_column :articles, :boosted, :boolean, default: false
end
end

View file

@ -0,0 +1,5 @@
class AddTaglineToOrganizations < ActiveRecord::Migration[5.1]
def change
add_column :organizations, :tag_line, :string
end
end

View file

@ -0,0 +1,5 @@
class AddPermitAdjacentSponsorsToUsers < ActiveRecord::Migration[5.1]
def change
add_column :users, :permit_adjacent_sponsors, :boolean, default: true
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180304151124) do
ActiveRecord::Schema.define(version: 20180316174324) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -106,6 +106,7 @@ ActiveRecord::Schema.define(version: 20180304151124) do
t.string "video_source_url"
t.string "video_thumbnail_url"
t.datetime "last_comment_at", default: "2017-01-01 05:00:00"
t.boolean "boosted", default: false
t.index ["featured_number"], name: "index_articles_on_featured_number"
t.index ["hotness_score"], name: "index_articles_on_hotness_score"
t.index ["published_at"], name: "index_articles_on_published_at"
@ -363,6 +364,7 @@ ActiveRecord::Schema.define(version: 20180304151124) do
t.datetime "updated_at", null: false
t.string "url"
t.string "zip_code"
t.string "tag_line"
t.index ["slug"], name: "index_organizations_on_slug", unique: true
end
@ -615,6 +617,7 @@ ActiveRecord::Schema.define(version: 20180304151124) do
t.string "username"
t.string "website_url"
t.datetime "workshop_expiration"
t.boolean "permit_adjacent_sponsors", default: true
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["language_settings"], name: "index_users_on_language_settings", using: :gin
t.index ["organization_id"], name: "index_users_on_organization_id"