Allow users to belong to multiple orgs (#2583)

* Allow user to have many orgs

* Allow users to handle multi orgs in settings

* Make rounded buttons inline

* Add multi org function to dashboards

* Fix merge conflicts

* Fix mistake in merge conflict fix oops

* Display the correct membership level

* Fix accessibility issues

* Display organizations for article editors

* Handle submitting org id with preact editors

* Make listings work with multiple organizations

* Allow listings to have multiple orgs on create

* Display the correct number of credits for each org

* Move script tag to Webpack

* Allow multi orgs for purchasing and viewing credits

* Use OrganizationMembership as authorization check

* Display multiple organizations for notifications

* Allow dashboard to be viewable under multi-orgs

* Remove unused method

* Add multi-org functionality for article editors

* Show pro dashboard buttons for member+ org levels

* Leave the correct organization

* Allow article API to change org id

* Add left-out authorization method oops

* Make nav buttons a bit more clear

* Fix merge conflict

* Fix adding org id for /api/articles and tests

* Fix tests for org policy

* Use proper logic for displaying org members

* Update org actions with new authorization

* Use correct org when creating a listing

* Remove additional payment charge oops

* Mark org notifications as read with authorization

* Remove deprecated post_as_organization attribute

* Use new org_admin syntax

* Remove deprecated org logic for article create and update

* Default all RSS posts to not belong to any org

* Render org_member page for guest users

* Update org policy spec to work with multi orgs

* Use org_membership for org traits and move identity code

* Use org_member trait

* Update to work with multi-orgs

* Validate article's org_id if param org_id is blank

* Make  a let variable

* Remove unnecessary eager load for credits

* Fix HTML structure and org logic for non-org users

* Update credits spec for multi-org

* Add test for failed payment when purchased by org

* Lint listings_spec

* Test that the listing was created under the user

* Add tests for POST /listings multi-org

* Use double quotes for classes

* Fix /manage and a few other multi-org bugs

* Fix test for multi org

* Use correct method SQL exists? not Rails exist?

* Fix reads spec for multi-org

* Fix org_controller actions to work with multi org

* Test only multi org and not old usage and fix leave_org

* Fix org showing user profile img test for multi-org

* Fix org logic for users with no orgs

* Remove switch org functionality

* Update tests and add hidden param for org id

* Redirect to the specific organization

* Test other org button actions

* Use settings_notice instead of legacy notice and refactor

* Fix weird extra end issue prob from merge conflicts

* Test for with new flash key

* Fix user_views_org tests for multi-org

* Test for new flash message

* Update snapshot with new a11y html

* Move styling to stylesheet

* Add site admins functionality

* Move org_member? method in user model and refactor

* Use unspent_credits_count for organizations

* Add tests for /listings/new and minor bug fixes

* Use .present? in case of empty array

* Fix a lingering deprecated method

* Use greater than 1 for random numbers

* Add tests for counting spent and unspent credits
This commit is contained in:
Andy Zhao 2019-06-04 09:30:52 -04:00 committed by Ben Halpern
parent c2d3384f80
commit 47d9ec27fb
81 changed files with 882 additions and 554 deletions

View file

@ -284,6 +284,9 @@
.listings-current-credits-inner {
display: inline-block;
margin: 20px;
select {
width: initial;
}
}
a {
text-decoration: underline;

View file

@ -258,6 +258,10 @@
font-size: 1.8em;
font-weight: bold;
padding: 30px 0px 50px;
.org-credits-select {
vertical-align: middle;
font-size: initial;
}
}
a {
color: white;

View file

@ -33,7 +33,7 @@
padding: 4px 20px;
border-radius: 100px;
margin-right: 5px;
display: block;
display: inline-block;
width: fit-content;
margin-bottom: 10px;
color: $black;

View file

@ -95,53 +95,13 @@
padding-left: 0px;
margin: 20px 2px 0px;
font-size: 16px;
cursor: pointer;
user-select: none;
img {
height: 26px;
width: 26px;
border-radius: 50px;
vertical-align: -7px;
margin-right: 4px;
}
button {
background: transparent;
border-radius: 3px;
font-size: 0.8em;
font-family: $helvetica-condensed;
width: 70px;
border-radius: 100px;
padding: 1px 0px;
margin-left: 10px;
border: var(--theme-container-border, 1px solid white);
&:focus {
outline: 0;
border: 1px solid $green;
}
&.no {
background: $light-gray;
background: var(--theme-background, $light-gray);
color: $medium-gray;
color: var(--theme-secondary-color, $medium-gray);
}
&.yes {
background: darken($green, 18%);
color: white;
}
select {
margin-left: 7px;
vertical-align: middle;
}
@media screen and (min-width: 600px) {
font-size: 18px;
padding-left: 10px;
img {
height: 29px;
width: 29px;
vertical-align: -8px;
margin-right: 8px;
}
button {
width: 120px;
}
}
}

View file

@ -301,6 +301,48 @@
color: $black;
background: $yellow;
}
.organization-dropdown {
position: relative;
display: inline-block;
color: $black;
background: $yellow;
}
select {
color: inherit;
background: inherit;
vertical-align: baseline;
font-size: inherit;
font-family: inherit;
font-stretch: inherit;
border: 0;
margin: 0;
padding-right: 1.25em;
text-indent: 0.01px;
-moz-appearance: none;
-webkit-appearance: none;
text-overflow: '';
}
.organization-dropdown::before,
.organization-dropdown::after {
content: "";
position: absolute;
pointer-events: none;
}
.organization-dropdown::after { /* Custom dropdown arrow */
content: "\25BC";
height: 1em;
font-size: 0.625em;
line-height: 1;
right: 0.25em;
top: 50%;
margin-top: -.5em;
}
.organization-dropdown::before { /* Custom dropdown arrow cover */
width: 1em;
right: 0;
top: 0;
bottom: 0;
}
}
.secret-field {
margin: 30px 0px;

View file

@ -9,7 +9,7 @@ module Api
data = if params[:organization_id]
org = Organization.find_by(id: params[:organization_id])
raise UnauthorizedError unless org && belongs_to_org?(user, org)
raise UnauthorizedError unless org && user.org_member?(org)
AnalyticsService.new(org, article_id: params[:article_id]).totals
else
@ -26,7 +26,7 @@ module Api
data = if params[:organization_id]
org = Organization.find_by(id: params[:organization_id])
raise UnauthorizedError unless org && belongs_to_org?(user, org)
raise UnauthorizedError unless org && user.org_member?(org)
AnalyticsService.new(org, start_date: params[:start], end_date: params[:end], article_id: params[:article_id]).stats_grouped_by_day
else
@ -40,7 +40,7 @@ module Api
data = if params[:organization_id]
org = Organization.find_by(id: params[:organization_id])
raise UnauthorizedError unless org && belongs_to_org?(user, org)
raise UnauthorizedError unless org && user.org_member?(org)
AnalyticsService.new(org, start_date: 1.day.ago, article_id: params[:article_id]).stats_grouped_by_day
else
@ -66,10 +66,6 @@ module Api
user
end
def belongs_to_org?(user, org)
user.organization_id == org.id
end
def valid_date_params?
date_regex = /\A\d{4}-\d{1,2}-\d{1,2}\Z/ # for example, 2019-03-22 or 2019-2-1
if params[:end]

View file

@ -72,11 +72,22 @@ module Api
def article_params
allowed_params = [
:title, :body_markdown, :published, :series, :publish_under_org,
:title, :body_markdown, :published, :series,
:main_image, :canonical_url, :description, tags: []
]
allowed_params << :organization_id if params["article"]["organization_id"] && allowed_to_change_org_id?
params.require(:article).permit(allowed_params)
end
def allowed_to_change_org_id?
potential_user = @article&.user || @user
if @article.nil? || OrganizationMembership.exists?(user: potential_user, organization_id: params["article"]["organization_id"])
OrganizationMembership.exists?(user: potential_user, organization_id: params["article"]["organization_id"])
elsif potential_user == @user
potential_user.org_admin?(params["article"]["organization_id"]) ||
@user.any_admin?
end
end
end
end
end

View file

@ -73,8 +73,9 @@ class ArticlesController < ApplicationController
def edit
authorize @article
@organization = @article.user&.organization
@version = @article.has_frontmatter? ? "v1" : "v2"
@user = @article.user
@organizations = @user&.organizations
end
def manage
@ -83,7 +84,8 @@ class ArticlesController < ApplicationController
@user = @article.user
@rating_vote = RatingVote.where(article_id: @article.id, user_id: @user.id).first
@buffer_updates = BufferUpdate.where(composer_user_id: @user.id, article_id: @article.id)
@organization = @user&.organization
@organizations = @user&.organizations
# TODO: fix this for multi orgs
@org_members = @organization.users.pluck(:name, :id) if @organization
end
@ -180,7 +182,7 @@ class ArticlesController < ApplicationController
def base_editor_assigments
@user = current_user
@version = @user.editor_version if @user
@organization = @user&.organization
@organizations = @user&.organizations
@tag = Tag.find_by(name: params[:template])
@prefill = params[:prefill].to_s.gsub("\\n ", "\n").gsub("\\n", "\n")
end
@ -235,11 +237,11 @@ class ArticlesController < ApplicationController
end
allowed_params = if params["article"]["version"] == "v1"
%i[body_markdown organization_id]
%i[body_markdown]
else
%i[
title body_markdown main_image published description
tag_list organization_id canonical_url series collection_id
tag_list canonical_url series collection_id
]
end
@ -247,12 +249,9 @@ class ArticlesController < ApplicationController
# fix the bug <https://github.com/thepracticaldev/dev.to/issues/2871>
if params["article"]["user_id"] && org_admin_user_change_privilege
allowed_params << :user_id
elsif params["article"]["post_under_org"].to_s == "true"
# add the organization of the article if explicitly asked to do so
params["article"]["organization_id"] = @user.organization_id
elsif params["article"]["post_under_org"].to_s == "false"
# remove the organization of the article if explicitly asked to do so
params["article"]["organization_id"] = nil
elsif params["article"]["organization_id"] && allowed_to_change_org_id?
# change the organization of the article only if explicitly asked to do so
allowed_params << :organization_id
end
params.require(:article).permit(allowed_params)
@ -272,10 +271,18 @@ class ArticlesController < ApplicationController
end
end
def allowed_to_change_org_id?
potential_user = @article&.user || current_user
potential_org_id = params["article"]["organization_id"].presence || @article&.organization_id
OrganizationMembership.exists?(user: potential_user, organization_id: potential_org_id) ||
current_user.any_admin?
end
def org_admin_user_change_privilege
params[:article][:user_id] &&
current_user.org_admin &&
current_user.organization_id == @article.organization_id &&
User.find(params[:article][:user_id])&.organization_id == @article.organization_id
# if current_user is an org admin of the article's org
current_user.org_admin?(@article.organization_id) &&
# and if the author being changed to belongs to the article's org
OrganizationMembership.exists?(user_id: params[:article][:user_id], organization_id: @article.organization_id)
end
end

View file

@ -17,11 +17,13 @@ class ClassifiedListingsController < ApplicationController
def new
@classified_listing = ClassifiedListing.new
@organizations = current_user.organizations
@credits = current_user.credits.where(spent: false)
end
def edit
authorize @classified_listing
@organizations = current_user.organizations
@credits = current_user.credits.where(spent: false)
end
@ -29,7 +31,7 @@ class ClassifiedListingsController < ApplicationController
@classified_listing = ClassifiedListing.new(classified_listing_params)
@classified_listing.user_id = current_user.id
@number_of_credits_needed = ClassifiedListing.cost_by_category(@classified_listing.category)
@org = Organization.find(current_user.organization_id) if @classified_listing.post_as_organization.to_i == 1
@org = Organization.find_by(id: @classified_listing.organization_id)
available_org_credits = @org.credits.where(spent: false) if @org
available_individual_credits = current_user.credits.where(spent: false)
@ -45,7 +47,8 @@ class ClassifiedListingsController < ApplicationController
def create_listing(credits)
@classified_listing.bumped_at = Time.current
@classified_listing.published = true
@classified_listing.organization_id = current_user.organization_id if @org
# this will 500 for now if they don't belong in the org
authorize @classified_listing, :authorized_organization_poster? if @classified_listing.organization_id.present?
if @classified_listing.save
clear_listings_cache
credits.limit(@number_of_credits_needed).update_all(spent: true)
@ -54,6 +57,7 @@ class ClassifiedListingsController < ApplicationController
else
@credits = current_user.credits.where(spent: false)
@classified_listing.cached_tag_list = classified_listing_params[:tag_list]
@organizations = current_user.organizations
render :new
end
end
@ -94,9 +98,9 @@ class ClassifiedListingsController < ApplicationController
@classified_listing = ClassifiedListing.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
# Never trust parameters from the scary internet, only allow a specific list through.
def classified_listing_params
accessible = %i[title body_markdown category tag_list contact_via_connect post_as_organization action]
accessible = %i[title body_markdown category tag_list contact_via_connect organization_id action]
params.require(:classified_listing).permit(accessible)
end

View file

@ -3,27 +3,32 @@ class CreditsController < ApplicationController
def index
@credits = current_user.credits.where(spent: false)
@org_credits = current_user.organization.credits.where(spent: false) if current_user.org_admin
@organizations = current_user.admin_organizations
end
def new
@credit = Credit.new
@purchaser = params[:purchaser] == "organization" ? current_user.organization : current_user
@purchaser = if params[:organization_id].present? && current_user.org_admin?(params[:organization_id])
Organization.find_by(id: params[:organization_id])
else
current_user
end
@organizations = current_user.admin_organizations
@customer = Stripe::Customer.retrieve(current_user.stripe_id_code) if current_user.stripe_id_code
end
def create
not_authorized if params[:organization_id].present? && !current_user.org_admin?(params[:organization_id])
@number_to_purchase = params[:credit][:number_to_purchase].to_i
return unless make_payment
credit_objects = []
@number_to_purchase.times do
if params[:user_type] == "organization"
raise unless current_user.org_admin
credit_objects << Credit.new(organization_id: current_user.organization_id, cost: cost_per_credit / 100.0)
credit_objects = Array.new(@number_to_purchase) do
if params[:organization_id].present?
Credit.new(organization_id: params[:organization_id], cost: cost_per_credit / 100.0)
else
credit_objects << Credit.new(user_id: current_user.id, cost: cost_per_credit / 100.0)
Credit.new(user_id: current_user.id, cost: cost_per_credit / 100.0)
end
end
Credit.import credit_objects

View file

@ -6,17 +6,17 @@ class DashboardsController < ApplicationController
def show
fetch_and_authorize_user
@org_dashboard = @user&.organization && @user&.org_admin && params[:which] == "organization"
target = @user
not_authorized if params[:org_id] && !@user.org_admin?(params[:org_id] || @user.any_admin?)
if @org_dashboard
articles = @user.organization.articles
@org_members = @user.organization.users.pluck(:name, :id)
else
articles = @user.articles.includes(:organization)
@organizations = @user.admin_organizations.includes(:users)
@member_organizations = @user.member_organizations
if params[:which] == "organization" && params[:org_id] && (@user.org_admin?(params[:org_id]) || @user.any_admin?)
target = @organizations.find_by(id: params[:org_id])
@organization = target
end
@articles = articles.sorting(params[:sort]).decorate
@articles = target.articles.sorting(params[:sort]).decorate
# Updates analytics in background if appropriate:
ArticleAnalyticsFetcher.new.delay.update_analytics(current_user.id) if @articles && ApplicationConfig["GA_FETCH_RATE"] < 50 # Rate limit concerned, sometimes we throttle down.
end
@ -53,6 +53,7 @@ class DashboardsController < ApplicationController
authorize current_user, :pro_user?
current_user
end
@organizations = current_user.member_organizations
@dashboard = Dashboard::Pro.new(user_or_org)
end

View file

@ -2,15 +2,11 @@ class Notifications::ReadsController < ApplicationController
def create
result = ""
result = ReadNotificationsService.new(current_user).mark_as_read if current_user
ReadNotificationsService.new(current_user.organization).mark_as_read if params[:org_id]&.to_i && user_belongs_to_org?
if params[:org_id] && current_user.org_member?(params[:org_id])
org = Organization.find_by(id: params[:org_id])
ReadNotificationsService.new(org).mark_as_read
end
current_user&.touch(:last_notification_activity)
render plain: result
end
private
def user_belongs_to_org?
# this can be changed later with roles
current_user.organization_id == params[:org_id].to_i
end
end

View file

@ -22,9 +22,7 @@ class NotificationsController < ApplicationController
@last_user_comment = @user.comments.last&.id
@notifications = @notifications.where("notified_at < ?", notified_at_offset) if notified_at_offset
@notifications = NotificationDecorator.decorate_collection(@notifications.limit(num))
org_id = params[:org_id] || current_user.organization_id
# in the future this can be an array of numbers, when people can belong to multiple orgs.
@total_org_unread = Notification.where(organization_id: org_id, user_id: nil, read: false).count
@organizations = @user.member_organizations if @user.organizations
render partial: "notifications_list" if notified_at_offset
end

View file

@ -8,14 +8,10 @@ class OrganizationsController < ApplicationController
@organization = Organization.new(organization_params)
authorize @organization
if @organization.save
ActiveRecord::Base.transaction do
current_user.update(organization_id: @organization.id, org_admin: true)
OrganizationMembership.create(organization_id: @organization.id, user_id: current_user.id, type_of_user: "admin")
end
redirect_to "/settings/organization", notice:
"Your organization was successfully created and you are an admin."
@organization_membership = OrganizationMembership.create!(organization_id: @organization.id, user_id: current_user.id, type_of_user: "admin")
flash[:settings_notice] = "Your organization was successfully created and you are an admin."
redirect_to "/settings/organization/#{@organization.id}"
else
@tab = "switch-organizations" if @user.has_role?(:switch_between_orgs)
render template: "users/edit"
end
end
@ -24,30 +20,31 @@ class OrganizationsController < ApplicationController
@user = current_user
@tab = "organization"
@tab_list = @user.settings_tab_list
@organization = @user.organization
@organization = Organization.find_by(id: organization_params[:id])
authorize @organization
if @organization.update(organization_params.merge(profile_updated_at: Time.current))
redirect_to "/settings/organization", notice: "Your organization was successfully updated."
flash[:settings_notice] = "Your organization was successfully updated."
redirect_to "/settings/organization"
else
render template: "users/edit"
end
end
def generate_new_secret
raise unless current_user.org_admin
@organization = current_user.organization
@organization = Organization.find_by(id: organization_params[:id])
authorize @organization
@organization.secret = @organization.generated_random_secret
@organization.save
redirect_to "/settings/organization", notice: "Your org secret was updated"
flash[:settings_notice] = "Your org secret was updated"
redirect_to "/settings/organization"
end
private
def permitted_params
accessible = %i[
id
name
summary
tag_line

View file

@ -1,6 +1,6 @@
class UsersController < ApplicationController
before_action :set_no_cache_header
after_action :verify_authorized, except: [:signout_confirm]
after_action :verify_authorized, except: %i[signout_confirm add_org_admin remove_org_admin remove_from_org]
# GET /settings/@tab
def edit
@ -26,8 +26,9 @@ class UsersController < ApplicationController
end
cookies.permanent[:user_experience_level] = @user.experience_level.to_s if @user.experience_level.present?
follow_hiring_tag(@user)
flash[:settings_notice] = notice
@user.touch(:profile_updated_at)
redirect_to "/settings/#{@tab}", notice: notice
redirect_to "/settings/#{@tab}"
else
render :edit
end
@ -41,10 +42,10 @@ class UsersController < ApplicationController
if @user.update(twitch_username: new_twitch_username)
@user.touch(:profile_updated_at)
Streams::TwitchWebhookRegistrationJob.perform_later(@user.id) if @user.twitch_username?
notice = "Your profile was successfully updated."
end
flash[:settings_notice] = "Your Twitch username was successfully updated."
end
redirect_to "/settings/#{@tab}", notice: notice
redirect_to "/settings/#{@tab}"
end
def update_language_settings
@ -52,9 +53,9 @@ class UsersController < ApplicationController
set_tabs("misc")
@user.language_settings["preferred_languages"] = Languages::LIST.keys & params[:user][:preferred_languages].to_a
if @user.save
notice = "Your profile was successfully updated."
flash[:settings_notice] = "Your language settings were successfully updated."
@user.touch(:profile_updated_at)
redirect_to "/settings/#{@tab}", notice: notice
redirect_to "/settings/#{@tab}"
else
render :edit
end
@ -66,8 +67,9 @@ class UsersController < ApplicationController
if @user.articles_count.zero? && @user.comments_count.zero?
@user.destroy!
NotifyMailer.account_deleted_email(@user).deliver
flash[:settings_notice] = "Your account has been deleted."
sign_out @user
redirect_to root_path, notice: "Your account has been deleted."
redirect_to root_path
else
flash[:error] = "An error occurred. Try requesting an account deletion below."
redirect_to "/settings/#{@tab}"
@ -86,12 +88,11 @@ class UsersController < ApplicationController
identity_username = "#{provider}_username".to_sym
@user.update(identity_username => nil, profile_updated_at: Time.current)
redirect_to "/settings/#{@tab}",
notice: "Your #{provider.capitalize} account was successfully removed."
flash[:settings_notice] = "Your #{provider.capitalize} account was successfully removed."
else
flash[:error] = "An error occurred. Please try again or send an email to: yo@dev.to"
redirect_to "/settings/#{@tab}"
end
redirect_to "/settings/#{@tab}"
end
def onboarding_update
@ -112,57 +113,55 @@ class UsersController < ApplicationController
def join_org
authorize User
if (@organization = Organization.find_by(secret: params[:org_secret].strip))
ActiveRecord::Base.transaction do
current_user.update(organization_id: @organization.id)
OrganizationMembership.create(user_id: current_user.id, organization_id: current_user.organization_id, type_of_user: "member")
end
redirect_to "/settings/organization",
notice: "You have joined the #{@organization.name} organization."
OrganizationMembership.create(user_id: current_user.id, organization_id: @organization.id, type_of_user: "member")
flash[:settings_notice] = "You have joined the #{@organization.name} organization."
redirect_to "/settings/organization/#{@organization.id}"
else
flash[:error] = "The given organization secret was invalid."
redirect_to "/settings/organization"
redirect_to "/settings/organization/new"
end
end
def leave_org
authorize User
type_of_user = current_user.org_admin ? "admin" : "member"
OrganizationMembership.find_by(organization_id: current_user.organization_id, user_id: current_user.id, type_of_user: type_of_user)&.delete
current_user.update(organization_id: nil, org_admin: nil)
redirect_to "/settings/organization",
notice: "You have left your organization."
org = Organization.find_by(id: params[:organization_id])
authorize org
OrganizationMembership.find_by(organization_id: org.id, user_id: current_user.id)&.delete
flash[:settings_notice] = "You have left your organization."
redirect_to "/settings/organization/new"
end
def add_org_admin
user = User.find(params[:user_id])
authorize user
user.update(org_admin: true)
org_membership = OrganizationMembership.find_or_initialize_by(user_id: user.id, organization_id: user.organization_id)
org_membership.type_of_user = "admin"
org_membership.save
redirect_to "/settings/organization",
notice: "#{user.name} is now an admin."
adminable = User.find(params[:user_id])
org = Organization.find_by(id: params[:organization_id])
not_authorized unless current_user.org_admin?(org) && OrganizationMembership.exists?(user: adminable, organization: org)
OrganizationMembership.find_by(user_id: adminable.id, organization_id: org.id).update(type_of_user: "admin")
flash[:settings_notice] = "#{adminable.name} is now an admin."
redirect_to "/settings/organization/#{org.id}"
end
def remove_org_admin
user = User.find(params[:user_id])
authorize user
user.update(org_admin: false)
org_membership = OrganizationMembership.find_or_initialize_by(user_id: user.id, organization_id: user.organization_id)
org_membership.type_of_user = "member"
org_membership.save
redirect_to "/settings/organization",
notice: "#{user.name} is no longer an admin."
unadminable = User.find(params[:user_id])
org = Organization.find_by(id: params[:organization_id])
not_authorized unless current_user.org_admin?(org) && unadminable.org_admin?(org)
OrganizationMembership.find_by(user_id: unadminable.id, organization_id: org.id).update(type_of_user: "member")
flash[:settings_notice] = "#{unadminable.name} is no longer an admin."
redirect_to "/settings/organization/#{org.id}"
end
def remove_from_org
user = User.find(params[:user_id])
authorize user
type_of_user = user.org_admin ? "admin" : "member"
OrganizationMembership.find_by(organization_id: current_user.organization_id, user_id: current_user.id, type_of_user: type_of_user)&.delete
user.update(organization_id: nil)
redirect_to "/settings/organization",
notice: "#{user.name} is no longer part of your organization."
removable = User.find(params[:user_id])
org = Organization.find_by(id: params[:organization_id])
removable_org_membership = OrganizationMembership.find_by(user_id: removable.id, organization_id: org.id)
not_authorized unless current_user.org_admin?(org) && removable_org_membership
removable_org_membership.delete
flash[:settings_notice] = "#{removable.name} is no longer part of your organization."
redirect_to "/settings/organization/#{org.id}"
end
def signout_confirm; end
@ -178,9 +177,7 @@ class UsersController < ApplicationController
case @tab
when "organization"
@organization = @user.organization || Organization.new
when "switch-organizations"
@organization = Organization.new
handle_organization_tab
when "integrations"
if current_user.identities.where(provider: "github").any?
@client = Octokit::Client.
@ -221,6 +218,19 @@ class UsersController < ApplicationController
private
def handle_organization_tab
@organizations = @current_user.organizations.order("name ASC")
if params[:org_id] == "new" || params[:org_id].blank? && @organizations.size.zero?
@organization = Organization.new
elsif params[:org_id].blank? || params[:org_id].match?(/\d/)
@organization = Organization.find_by(id: params[:org_id]) || @organizations.first
authorize @organization, :part_of_org?
@org_organization_memberships = @organization.organization_memberships.includes(:user)
@organization_membership = OrganizationMembership.find_by(user_id: current_user.id, organization_id: @organization.id)
end
end
def set_user
@user = current_user
authorize @user

View file

@ -46,16 +46,20 @@ exports[`<ArticleForm /> renders properly 1`] = `
<button
class="articleform__detailsButton articleform__detailsButton--image"
onClick={[Function]}
type="button"
>
<img
alt="Upload an image"
src=""
/>
</button>
<button
class="articleform__detailsButton articleform__detailsButton--moreconfig"
onClick={[Function]}
type="button"
>
<img
alt="Menu"
src=""
/>
</button>
@ -79,6 +83,7 @@ exports[`<ArticleForm /> renders properly 1`] = `
<button
class="articleform__detailsButton articleform__detailsButton--image articleform__detailsButton--bottom"
onClick={[Function]}
type="button"
>
<img
alt="upload images"

View file

@ -21,8 +21,8 @@ export default class ArticleForm extends Component {
constructor(props) {
super(props);
this.article = JSON.parse(this.props.article);
const organization = this.props.organization
? JSON.parse(this.props.organization)
const organizations = this.props.organizations
? JSON.parse(this.props.organizations)
: null;
this.url = window.location.href;
@ -46,8 +46,8 @@ export default class ArticleForm extends Component {
imageManagementShowing: false,
moreConfigShowing: false,
mainImage: this.article.main_image || null,
organization,
postUnderOrg: !!this.article.organization_id,
organizations,
organizationId: this.article.organization_id,
errors: null,
edited: false,
version: this.props.version,
@ -155,10 +155,10 @@ export default class ArticleForm extends Component {
}
};
toggleOrgPosting = e => {
e.preventDefault();
this.setState({ postUnderOrg: !this.state.postUnderOrg });
};
handleOrgIdChange = e => {
const organizationId = e.target.selectedOptions[0].value;
this.setState({ organizationId })
}
failedPreview = response => {
console.log(response);
@ -271,8 +271,8 @@ export default class ArticleForm extends Component {
submitting,
imageManagementShowing,
moreConfigShowing,
organization,
postUnderOrg,
organizations,
organizationId,
mainImage,
errors,
version
@ -303,11 +303,11 @@ export default class ArticleForm extends Component {
) : (
''
);
const orgArea = organization ? (
const orgArea = organizations ? (
<OrgSettings
organization={organization}
postUnderOrg={postUnderOrg}
onToggle={this.toggleOrgPosting}
organizations={organizations}
organizationId={organizationId}
onToggle={this.handleOrgIdChange}
/>
) : (
''
@ -354,14 +354,16 @@ export default class ArticleForm extends Component {
<button
className="articleform__detailsButton articleform__detailsButton--image"
onClick={this.toggleImageManagement}
type="button"
>
<img src={ImageUploadIcon} />
<img src={ImageUploadIcon} alt="Upload an image" />
</button>
<button
className="articleform__detailsButton articleform__detailsButton--moreconfig"
onClick={this.toggleMoreConfig}
type="button"
>
<img src={ThreeDotsIcon} />
<img src={ThreeDotsIcon} alt="Menu" />
</button>
</div>
</div>
@ -381,6 +383,7 @@ export default class ArticleForm extends Component {
<button
className="articleform__detailsButton articleform__detailsButton--image articleform__detailsButton--bottom"
onClick={this.toggleImageManagement}
type="button"
>
<img src={ImageUploadIcon} alt="upload images" />
{' '}

View file

@ -1,24 +1,33 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
const OrgSettings = ({ organization, postUnderOrg, onToggle }) => (
<div className="articleform__orgsettings" onClick={onToggle}>
<img
src={organization.profile_image_90}
style={{ opacity: postUnderOrg ? '1' : '0.7' }}
/>
{' '}
{organization.name}
{' '}
<button className={postUnderOrg ? 'yes' : 'no'}>
{postUnderOrg ? '✅ YES' : '◻️ NO'}
</button>
const orgOptions = (organizations, organizationId) => {
const orgs = organizations.map((organization) => {
if(organizationId === organization.id) {
return(
<option value={organization.id} selected>{organization.name}</option>
)
}
return (
<option value={organization.id}>{organization.name}</option>
)
})
const nullOrgOption = organizationId === null ? <option value="" selected>None</option> : <option value="">None</option>
orgs.unshift(nullOrgOption) // make first option as "None"
return orgs
}
const OrgSettings = ({ organizations, organizationId, onToggle }) => (
<div className="articleform__orgsettings">
Publish under an organization:
<select name="article[organization_id]" id="article_publish_under_org" onBlur={onToggle}>
{orgOptions(organizations, organizationId)}
</select>
</div>
);
OrgSettings.propTypes = {
onChange: PropTypes.func.isRequired,
defaultValue: PropTypes.string.isRequired,
onToggle: PropTypes.func.isRequired,
};
export default OrgSettings;

View file

@ -0,0 +1,35 @@
// listings
import { h } from 'preact';
import PropTypes from 'prop-types';
const orgOptions = (organizations, organizationId) => {
const orgs = organizations.map((organization) => {
if (organizationId === organization.id) {
return (
<option value={organization.id} selected>{organization.name}</option>
)
}
return (
<option value={organization.id}>{organization.name}</option>
)
})
const nullOrgOption = organizationId === null ? <option value="" selected>None</option> : <option value="">None</option>
orgs.unshift(nullOrgOption) // make first option as "None"
return orgs
}
const OrgSettings = ({ organizations, organizationId, onToggle }) => (
<div className="field">
<label htmlFor="organizationId">Post under an organization:</label>
<select name="classified_listing[organization_id]" id="listing_organization_id" onBlur={onToggle}>
{orgOptions(organizations, organizationId)}
</select>
<p><em>Posting on behalf of org spends org credits.</em></p>
</div>
);
OrgSettings.propTypes = {
onToggle: PropTypes.func.isRequired,
};
export default OrgSettings;

View file

@ -4,7 +4,7 @@ import Title from './elements/title';
import BodyMarkdown from './elements/bodyMarkdown';
import Categories from './elements/categories';
import Tags from './elements/tags';
import OrgSettings from './elements/orgSettings';
export default class ListingForm extends Component {
constructor(props) {
@ -14,8 +14,7 @@ export default class ListingForm extends Component {
this.categoriesForDetails = JSON.parse(this.props.categoriesForDetails);
this.categoriesForSelect = JSON.parse(this.props.categoriesForSelect);
const organizations = this.props.organizations
? JSON.parse(this.props.organizations) : null;
const organizations = JSON.parse(this.props.organizations);
this.url = window.location.href;
@ -25,12 +24,18 @@ export default class ListingForm extends Component {
category: this.listing.category || '',
tagList: this.listing.cached_tag_list || '',
bodyMarkdown: this.listing.body_markdown || '',
organizations: organizations || null,
categoriesForSelect: this.categoriesForSelect,
categoriesForDetails: this.categoriesForDetails,
organizations,
organizationId: null, // change this for /edit later
}
}
handleOrgIdChange = e => {
const organizationId = e.target.selectedOptions[0].value;
this.setState({ organizationId })
}
render() {
const {
id,
@ -38,10 +43,20 @@ export default class ListingForm extends Component {
bodyMarkdown,
tagList,
category,
organizations,
categoriesForDetails,
categoriesForSelect,
organizations,
organizationId,
} = this.state;
const orgArea = organizations ? (
<OrgSettings
organizations={organizations}
organizationId={organizationId}
onToggle={this.handleOrgIdChange}
/>
) : (
''
);
if (id === null) {
return(
<div>
@ -49,15 +64,18 @@ export default class ListingForm extends Component {
<BodyMarkdown defaultValue={bodyMarkdown} onChange={linkState(this, 'bodyMarkdown')} />
<Categories categoriesForSelect={categoriesForSelect} categoriesForDetails={categoriesForDetails} onChange={linkState(this, 'category')} category={category} />
<Tags defaultValue={tagList} category={category} onInput={linkState(this, 'tagList')} />
{orgArea}
{/* add contact via connect checkbox later */}
</div>
)
}
// WIP code for edit
return(
<div>
<Title defaultValue={title} onChange={linkState(this, 'title')} />
<BodyMarkdown defaultValue={bodyMarkdown} onChange={linkState(this, 'bodyMarkdown')} />
<Tags defaultValue={tagList} onInput={linkState(this, 'tagList')} />
{orgArea}
{/* add contact via connect checkbox later */}
</div>
)

View file

@ -25,10 +25,10 @@ function loadForm() {
window.csrfToken = csrfToken;
const root = document.getElementById('article-form');
const { article, organization, version } = root.dataset;
const { article, organizations, version } = root.dataset;
render(
<ArticleForm article={article} organization={organization} version={version} />,
<ArticleForm article={article} organizations={organizations} version={version} />,
root,
root.firstElementChild,
);

View file

@ -0,0 +1,15 @@
const orgCreditsSelect = document.getElementById('org-credits-select')
const orgCreditsNumber = document.getElementById('org-credits-number')
const orgCreditsLink = document.getElementById('org-credits-purchase-link')
orgCreditsNumber.innerText = orgCreditsSelect.selectedOptions[0].dataset.credits
const changeOrgCredits = (event) => {
const selectedOrgCreditsCount = event.target.selectedOptions[0].dataset.credits
const selectedOrgId = event.target.selectedOptions[0].value
orgCreditsNumber.innerText = selectedOrgCreditsCount
orgCreditsLink.href = `/credits/purchase?organization_id=${selectedOrgId}`
}
orgCreditsSelect.addEventListener('change', changeOrgCredits)

View file

@ -15,9 +15,12 @@ class Article < ApplicationRecord
belongs_to :user
belongs_to :job_opportunity, optional: true
counter_culture :user
belongs_to :organization, optional: true
belongs_to :collection, optional: true
counter_culture :user
counter_culture :organization
has_many :comments, as: :commentable, inverse_of: :commentable
has_many :buffer_updates
has_many :notifications, as: :notifiable, inverse_of: :notifiable

View file

@ -1,7 +1,7 @@
class ClassifiedListing < ApplicationRecord
include AlgoliaSearch
attr_accessor :post_as_organization, :action
attr_accessor :action
belongs_to :user, optional: true
belongs_to :organization, optional: true

View file

@ -4,8 +4,20 @@ class Credit < ApplicationRecord
belongs_to :user, optional: true
belongs_to :organization, optional: true
counter_culture :user # Only counting credits_count right now. TODO: spent_credits_count and unspent_credits_count (counter culture gem)
counter_culture :organization # Only counting credits_count right now. TODO: spent_credits_count and unspent_credits_count (counter culture gem)
counter_culture :user,
column_name: proc { |model| "#{model.spent ? 'spent' : 'unspent'}_credits_count" },
column_names: {
["credits.spent = ?", true] => "spent_credits_count",
["credits.spent = ?", false] => "unspent_credits_count",
["credits.id > ?", 0] => "credits_count"
}
counter_culture :organization,
column_name: proc { |model| "#{model.spent ? 'spent' : 'unspent'}_credits_count" },
column_names: {
["credits.spent = ?", true] => "spent_credits_count",
["credits.spent = ?", false] => "unspent_credits_count",
["credits.id > ?", 0] => "credits_count"
}
def self.add_to(user, amount)
credit_objects = []

View file

@ -4,13 +4,15 @@ class Organization < ApplicationRecord
acts_as_followable
has_many :job_listings
has_many :users
has_many :organization_memberships
has_many :users, through: :organization_memberships
has_many :api_secrets, through: :users
has_many :articles
has_many :collections
has_many :display_ads
has_many :notifications
has_many :credits
has_many :unspent_credits, -> { where spent: false }, class_name: "Credit", inverse_of: :organization
has_many :classified_listings
validates :name, :summary, :url, :profile_image, presence: true

View file

@ -1,4 +1,7 @@
class OrganizationMembership < ApplicationRecord
belongs_to :user
belongs_to :organization
validates :user_id, :organization_id, :type_of_user, presence: true
validates :user_id, uniqueness: { scope: :organization_id }
validates :type_of_user, inclusion: { in: %w[admin member guest] }

View file

@ -18,7 +18,6 @@ class Role < ApplicationRecord
trusted
banned
warned
switch_between_orgs
triple_unicorn_member
level_4_member
level_3_member

View file

@ -10,7 +10,8 @@ class User < ApplicationRecord
acts_as_followable
acts_as_follower
belongs_to :organization, optional: true
has_many :organization_memberships
has_many :organizations, through: :organization_memberships
has_many :api_secrets, dependent: :destroy
has_many :articles, dependent: :destroy
has_many :badge_achievements, dependent: :destroy
@ -351,8 +352,22 @@ class User < ApplicationRecord
has_any_role?(:workshop_pass, :level_3_member, :level_4_member, :triple_unicorn_member)
end
def admin_organizations
org_ids = organization_memberships.where(type_of_user: "admin").pluck(:organization_id)
organizations.where(id: org_ids)
end
def member_organizations
org_ids = organization_memberships.where(type_of_user: %w[admin member]).pluck(:organization_id)
organizations.where(id: org_ids)
end
def org_member?(organization)
OrganizationMembership.exists?(user: user, organization: organization, type_of_user: %w[admin member])
end
def org_admin?(organization)
user.org_admin && user.organization_id == organization.id
OrganizationMembership.exists?(user: user, organization: organization, type_of_user: "admin")
end
def unique_including_orgs

View file

@ -41,6 +41,6 @@ class ArticlePolicy < ApplicationPolicy
end
def user_org_admin?
user.org_admin && user.organization_id == record.organization_id
user.org_admin?(record.organization_id)
end
end

View file

@ -7,6 +7,10 @@ class ClassifiedListingPolicy < ApplicationPolicy
user_is_author?
end
def authorized_organization_poster?
user.org_member?(record.organization_id)
end
private
def user_is_author?

View file

@ -7,11 +7,21 @@ class OrganizationPolicy < ApplicationPolicy
user.org_admin?(record)
end
def leave_org?
part_of_org?
end
def part_of_org?
return false if record.blank?
OrganizationMembership.exists?(user_id: user.id, organization_id: record.id)
end
def generate_new_secret?
update?
end
def pro_org_user?
user.has_role?(:pro) && user.organization_id == record.id
user.has_role?(:pro) && OrganizationMembership.exists?(user_id: user.id, organization_id: record.id)
end
end

View file

@ -28,19 +28,7 @@ class UserPolicy < ApplicationPolicy
end
def leave_org?
true
end
def add_org_admin?
user.org_admin && within_the_same_org?
end
def remove_org_admin?
user.org_admin && not_self? && within_the_same_org?
end
def remove_from_org?
user.org_admin && not_self? && within_the_same_org?
OrganizationMembership.exists?(user_id: user.id, organization_id: record.id)
end
def remove_association?
@ -127,10 +115,6 @@ class UserPolicy < ApplicationPolicy
private
def within_the_same_org?
user.organization == record.organization
end
def not_self?
user != record
end

View file

@ -9,10 +9,6 @@ class ArticleCreationService
tags = article_params[:tags]
series = article_params[:series]
publish_under_org = (
article_params[:publish_under_org] == true ||
article_params[:publish_under_org].to_i == 1
)
# convert tags from array to a string
if tags.present?
@ -23,7 +19,6 @@ class ArticleCreationService
article = Article.new(article_params)
article.user_id = user.id
article.show_comments = true
article.organization = user.organization if publish_under_org
article.collection = Collection.find_series(series, user) if series.present?
if article.save

View file

@ -27,9 +27,6 @@ module Articles
article_params.delete(:tags)
end
# add organization if requested
article_params[:organization_id] = user.organization_id if article_params[:publish_under_org] == true
# updated edited time only if already published and not edited by an admin
update_edited_at = article.user == user && article.published
article_params[:edited_at] = Time.current if update_edited_at

View file

@ -77,7 +77,7 @@ class RssReader
published_from_feed: true,
show_comments: true,
body_markdown: RssReader::Assembler.call(item, user, feed, feed_source_url),
organization_id: user.organization_id.presence,
organization_id: nil,
)
send_slack_notification(article)

View file

@ -2,16 +2,15 @@
<div class="articleform"
id="article-form"
data-article="<%= article.to_json(only: %i[id title cached_tag_list published body_markdown main_image organization_id canonical_url], methods: %i[series all_series]) %>"
data-organization="<%= organization&.to_json(only: %i[name bg_color_hex text_color_hex], methods: [:profile_image_90]) %>"
data-organizations="<%= organizations&.to_json(only: %i[id name bg_color_hex text_color_hex], methods: [:profile_image_90]) %>"
data-version="<%= version %>">
<form class="articleform__form articleform__form--<%= version %>">
<% if organization %>
<% if organizations %>
<div class="articleform__orgsettings">
<img src="<%= organization.profile_image_90 %>" style="opacity: <%= article.organization ? "1.0" : "0.7" %>" alt="<%= organization.name %> profile image">
<%= organization.name %>
<button class="<%= article.organization ? "yes" : "no" %>">
<%= article.organization ? "✅ YES" : "◻️ NO" %>
</button>
Publish under an organization:
<select name="article[organization_id]" id="article_publish_under_org">
<%= options_for_select({ "None" => "" }.merge(organizations.pluck(:name, :id).to_h), article.organization_id || "") %>
</select>
</div>
<% end %>
<% if version == "v2" %>

View file

@ -28,4 +28,4 @@
<% end %>
<%= javascript_pack_tag "articleForm", defer: true %>
<%= render "articles/v2_form", article: @article, organization: @organization, version: @version %>
<%= render "articles/v2_form", article: @article, organizations: @organizations, version: @version %>

View file

@ -23,7 +23,7 @@
</div>
<div class="dashboard-container">
<%= render "dashboards/dashboard_article", article: @article, org_admin: @user.org_admin, manage_view: true %>
<%= render "dashboards/dashboard_article", article: @article, organization: @article.organization, org_admin: @user.org_admin?(@article.organization), manage_view: true %>
<% if @buffer_updates.any? %>
<div class="single-article thanks-for-suggesting-a-tweet">
Tweet Suggestion Sent 🙏

View file

@ -2,7 +2,7 @@
<% if user_signed_in? %>
<%= javascript_pack_tag "articleForm", defer: true %>
<%= render "articles/v2_form", article: @article, organization: @organization, version: @version %>
<%= render "articles/v2_form", article: @article, organizations: @organizations, version: @version %>
<% else %>
<% @new_article_not_logged_in = true %>
<%= render "devise/registrations/registration_form" %>

View file

@ -1,13 +1,12 @@
<%= form_with(model: classified_listing, local: true) do |form| %>
<header>
<a href="/listings" class="listings-back-button">< go back to listings</a>
<a href="/listings" class="listings-back-button">&lt; go back to listings</a>
<h2>Create a Community Listing</h2>
</header>
<div class="classified-form-inner">
<% if classified_listing.errors.any? %>
<div class="classified-errors">
<h2><%= pluralize(classified_listing.errors.count, "error") %> prohibited this listing from being saved:</h2>
<ul>
<% classified_listing.errors.full_messages.each do |message| %>
<li><%= message %></li>
@ -17,11 +16,11 @@
<% end %>
<div id="listingform-data"
data-listing="<%= classified_listing.to_json(only: %i[id title body_markdown category cached_tag_list]) %>"
data-organizations="<%= @organizations.to_json %>"
data-organizations="<%= @organizations.to_json(only: %i[id name]) %>"
data-categories-for-select="<%= ClassifiedListing.select_options_for_categories.to_json %>"
data-categories-for-details="<%= ClassifiedListing.categories_available.transform_values{ |value_hash| value_hash.except(:cost) }.values.to_json %>"
data-categories-for-details="<%= ClassifiedListing.categories_available.transform_values { |value_hash| value_hash.except(:cost) }.values.to_json %>"
>
<div style="height: 650px;">
<div style="height: 745px;">
<div class="field">
<%= form.label "title" %>
<%= form.text_field "title", placeholder: "128 characters max, plain text" %>
@ -46,41 +45,48 @@
<%= form.label "tags_list", "Tags" %>
<%= form.text_field "tags_list", placeholder: "8 tags max, comma separated, no spaces or special characters" %>
</div>
<% if @organizations.present? %>
<div class="field">
<%= form.label "organization_id", "Post under an organization:" %>
<%= form.select :organization_id, "None" => "None" %>
<p><em>Posting on behalf of an organization spends the organization's credits.</em></p>
</div>
<% end %>
</div>
</div>
<%= javascript_pack_tag "listingForm", defer: true %>
<%= javascript_pack_tag "listingForm", defer: true %>
<div class="field">
<%= form.label "contact_via_connect", "Allow Users to Message Me Via In-App Chat (DEV Connect)" %>
<%= form.check_box "contact_via_connect" %>
</div>
<% if current_user.organization && current_user.org_admin %>
<div class="field">
<%= form.label "post_as_organization", "Post as #{current_user.organization.name} organization" %>
<%= form.check_box "post_as_organization" %>
</div>
<p><em>Posting on behalf of org spends org credits.</em></p>
<% end %>
<div class="actions">
<% if @credits.size > 0 || (current_user.organization && current_user.organization.credits.where(spent: false).size > 0)%>
<% if @credits.size > 0 || (@organizations.present? && @organizations.sum(:unspent_credits_count) > 0) %>
<%= form.submit "CREATE LISTING", class: "cta cta-main-listing-form" %>
<% else %>
<p>You need at least one credit to create a listing.</p>
<a href="/credits/new" class="cta cta-main-listing-form">Purchase Credits Now</a>
<a href="/credits/new" class="cta cta-main-listing-form" data-no-instant>Purchase Credits Now</a>
<% end %>
</div>
<p><em><%= ApplicationConfig["COMMUNITY_NAME"] %> admins may modify tags and categories to ensure accuracy. We may also take down and refund inappropriate listings.</em></p>
<p><em>Listings expire 30 days after they are published or "bumped".</em></p>
<div class="listings-current-credits">
<% if @credits.size == 0 && (!current_user.organization || current_user.organization.credits.where(spent: false).size == 0) %>
<% if @credits.size == 0 && (@organizations.blank? || @organizations.sum(:unspent_credits_count).zero?) %>
<h3>Listings Require Credits</h3>
<% end %>
<div class="listings-current-credits-inner">
You have <%= pluralize @credits.size, "credit" %> available — <a href="/credits/purchase" data-no-instant>Buy More</a>
</div>
<% if current_user.organization && current_user.org_admin %>
<% if @organizations.present? %>
<div class="listings-current-credits-inner">
<%= current_user.organization.name %> has <%= current_user.organization.credits.where(spent: false).size %> — <a href="/credits/purchase?purchaser=organization" data-no-instant>Buy More</a>
<select id="org-credits-select" class="org-credits-select">
<% @organizations.each do |org| %>
<option value="<%= org.id %>" data-credits="<%= org.unspent_credits_count %>"><%= org.name %></option>
<% end %>
</select>
has <span id="org-credits-number"><%= @organizations.first.unspent_credits_count %></span> credits -
<a id="org-credits-purchase-link" href="/credits/purchase?organization_id=<%= @organizations.first.id %>" data-no-instant>Buy More</a>
</div>
<%= javascript_pack_tag "orgCreditsSelector", defer: true %>
<% end %>
</div>
</div>

View file

@ -8,16 +8,23 @@
<div class="existing-credits-status">You have <%= @credits.size %> credits to spend</div>
<a href="/credits/purchase" data-no-instant>Purchase additional credits</a>
</div>
<% if current_user.organization && current_user.org_admin %>
<div class="existing-credits">
<div class="existing-credits-status"><%= current_user.organization.name %> has <%= @org_credits.size %> credits to spend</div>
<a href="/credits/purchase?purchaser=organization" data-no-instant>Purchase additional credits</a>
<% if @organizations.present? %>
<div class="existing-credits">
<div class="existing-credits-status">
<select id="org-credits-select" class="org-credits-select">
<% @organizations.each do |org| %>
<option value="<%= org.id %>" data-credits="<%= org.unspent_credits_count %>"><%= org.name %></option>
<% end %>
</select>
has <span id="org-credits-number"><%= @organizations.first.unspent_credits_count %></span> credits to spend
</div>
<a id="org-credits-purchase-link" href="/credits/purchase?organization_id=<%= @organizations.first.id %>" data-no-instant>Purchase additional credits</a>
<%= javascript_pack_tag "orgCreditsSelector", defer: true %>
</div>
<% end %>
<% if @credits.size > 0 %>
<center>
<a href="/listings/new">Create a Listing</a>
</center>
<% end %>
</div>
</div>

View file

@ -3,13 +3,16 @@
<div class="credits-container">
<div class="credits-banner">
<h1>Purchase credits for @<%= @purchaser.username %></h1>
<% if current_user.org_admin %>
<% if @purchaser.username == current_user.username %>
<a href="/credits/purchase?purchaser=organization" data-no-instant>Switch to <%= current_user.organization.name %></a>
<% else %>
<% if @organizations %>
<% @organizations.each do |org| %>
<% unless @purchaser.id == org.id %>
<a href="/credits/purchase?organization_id=<%= org.id %>" data-no-instant>Switch to <%= org.name %></a>
<% end %>
<% end %>
<% end %>
<% unless @purchaser.is_a? User %>
<a href="/credits/purchase" data-no-instant>Switch to personal purchase</a>
<% end %>
<% end %>
</div>
<script src="https://js.stripe.com/v3/"></script>
<p class="hook-description">Credits are used for <a href="/listings">DEV listings</a> as well as upcoming products and services.</p>
@ -61,11 +64,11 @@
</details>
<details>
<summary>
How many credits <%= params[:purchaser] == "organization" ? "does #{@purchaser.name}" : "do I currently" %> have?
How many credits <%= params[:organization_id].present? ? "does #{@purchaser.name}" : "do I currently" %> have?
</summary>
<div>
<p>
<%= params[:purchaser] == "organization" ? "#{@purchaser.name} has" : "You have" %> <%= @purchaser.credits.where(spent: false).size %> credits.
<%= params[:organization_id].present? ? "#{@purchaser.name} has" : "You have" %> <%= @purchaser.credits.where(spent: false).size %> credits.
</p>
</div>
</details>
@ -85,17 +88,15 @@
</label>
<div class="amount-wrapper">
<%= f.number_field :number_to_purchase,
id: "amount-input",
placeholder: "Enter any amount", autocomplete: "off",
min: "0",
max: "10000000",
required: true %>
id: "amount-input",
placeholder: "Enter any amount", autocomplete: "off",
min: "0",
max: "10000000",
required: true %>
<div id="calculated-price"></div>
</div>
<% if params[:purchaser] == "organization" %>
<div>
<input type="hidden" name="user_type" value="organization">
</div>
<% if @purchaser.is_a? Organization %>
<input type="hidden" name="organization_id" value="<%= @purchaser.id %>">
<% end %>
<div class="form-row">
<label for="card-element" class="main-form-label">
@ -125,7 +126,7 @@
<div id="card-errors" role="alert"></div>
</div>
<button id="add-credit-card-button" class="credit-card-button">Complete Purchase</button>
<% if params[:purchaser] == "organization" %>
<% if @purchaser.is_a? Organization %>
<p><em>*All org admins have access to credits owned by organization</em></p>
<% end %>
<% end %>

View file

@ -54,7 +54,7 @@
<% end %>
<% if article.published %>
<span id="pageviews-<%= article.id %>" class="cta pill dashboard-pageviews-indicator" data-analytics-pageviews data-article-id="<%= article.id %>">
<span class='page-views-count'>
<span class="page-views-count">
<span class="value">
<% if article.page_views_count > 100 %>
<span class="value"><%= article.page_views_count %></span>
@ -65,14 +65,14 @@
views
</span>
//
<span class='reactions-count'>
<span class="reactions-count">
<span class="value">
<%= article.positive_reactions_count %>
</span>
<%= "reaction".pluralize(count: article.positive_reactions_count) %>
</span>
//
<span class='comments-count'>
<span class="comments-count">
<span class="value">
<%= article.comments_count %>
</span>
@ -81,10 +81,10 @@
</span>
<% end %>
</div>
<% if org_admin %>
<% if organization && org_admin %>
<%= form_for(article) do |f| %>
<input type="hidden" name="destination" value="/dashboard" />
AUTHOR: <%= f.select(:user_id, options_for_select(@org_members, article.user_id)) %>
<input type="hidden" name="destination" value="/dashboard/organization/<%= organization.id %>" />
AUTHOR: <%= f.select(:user_id, options_for_select(organization.users.pluck(:name, :id), article.user_id)) %>
<%= f.submit "SUBMIT AUTHOR CHANGE", class: "cta pill black" %>
<% end %>
<% end %>

View file

@ -2,15 +2,18 @@
<div class="dashboard-container" id="user-dashboard">
<%= render "actions" %>
<% if @user.org_admin && @user.organization %>
<% if @organizations && (params[:which].blank? || params[:which] == "organization") %>
<h1>
<a href="/dashboard/user_followers" class="rounded-btn <%= "active" if params[:which] == "user_followers" %>">Personal</a>
<a href="/dashboard/organization_user_followers" class="rounded-btn <%= "active" if params[:which] == "organization_user_followers" %>"><%= @user.organization.name %> (<%= @user.organization.followers_count %>)</a>
<% if @user.has_role? :pro %>
<a class="rounded-btn" href="/dashboard/pro">Pro Analytics</a>
<a href="/dashboard" class="rounded-btn <%= "active" if params[:which].blank? %>">Personal</a>
<% @organizations.each do |org| %>
<a href="/dashboard/organization/<%= org.id %>" class="rounded-btn <%= "active" if params[:org_id].to_i == org.id %>"><%= org.name %> (<%= org.articles.size %>)</a>
<% end %>
<% if @user.has_role?(:pro) && @user.organization_id %>
<a class="rounded-btn" href="/dashboard/pro/org/<%= @user.organization_id %>">Pro Analytics for <%= @user.organization.name %></a>
<% if @user.has_role? :pro %>
<br>
<a class="rounded-btn" href="/dashboard/pro">Pro Analytics</a>
<% @organizations.each do |org| %>
<a class="rounded-btn" href="/dashboard/pro/org/<%= org.id %>">Pro Analytics for <%= org.name %></a>
<% end %>
<% end %>
</h1>
<% end %>

View file

@ -1,17 +1,15 @@
<div class="dashboard-container pro-container" id="user-dashboard">
<a href="/dashboard" class="rounded-btn">Back to Dashboard</a>
<% current_active_view = @dashboard.user_or_org.class.name %>
<% if current_active_view == "Organization" %>
<a class="rounded-btn" href="/dashboard/pro">
View Your Pro Dashboard
<a href="/dashboard" class="rounded-btn">Back to Main Dashboard</a>
<a class="rounded-btn <%= "active" if params[:org_id].blank? %>" href="/dashboard/pro">
Your Pro Dashboard
</a>
<% elsif current_active_view == "User" && @dashboard.user_or_org.organization_id %>
<a class="rounded-btn" href="/dashboard/pro/org/<%= @dashboard.user_or_org.organization_id %>">
View <%= @dashboard.user_or_org.organization.name %> Dashboard
</a>
<% end %>
<% @organizations.each do |org| %>
<a class="rounded-btn <%= "active" if params[:org_id].to_i == org.id %>" href="/dashboard/pro/org/<%= org.id %>">
<%= org.name %> Pro Dashboard
</a>
<% end %>
<section class="header-card card">
<h1 class="pro-header">Pro Dashboard <%= "for #{@dashboard.user_or_org.name}" if current_active_view == "Organization" %></h1>
<h1 class="pro-header">Pro Dashboard <%= "for #{@dashboard.user_or_org.name}" %></h1>
<p>Welcome to the pro dashboard which shows in-depth user metrics so that authors can make data-driven decisions about the developer ecosystem.</p>
<p>This dashboard will highlight deep insights especially relevant to developer relations authors and serious bloggers.</p>
</section>

View file

@ -2,22 +2,25 @@
<div class="dashboard-container" id="user-dashboard">
<%= render "actions" %>
<% if @user.org_admin && @user.organization && (params[:which].blank? || params[:which] == "organization") %>
<% if @organizations && (params[:which].blank? || params[:which] == "organization") %>
<h1>
<a href="/dashboard" class="rounded-btn <%= "active" if params[:which].blank? %>">Personal</a>
<a href="/dashboard/organization" class="rounded-btn <%= "active" if params[:which] == "organization" %>"><%= @user.organization.name %> (<%= @user.organization.articles.size %>)</a>
<% if @user.has_role? :pro %>
<a class="rounded-btn" href="/dashboard/pro">Pro Analytics</a>
<% @organizations.each do |org| %>
<a href="/dashboard/organization/<%= org.id %>" class="rounded-btn <%= "active" if params[:org_id].to_i == org.id %>"><%= org.name %> (<%= org.articles_count %>)</a>
<% end %>
<% if @user.has_role?(:pro) && @user.organization_id %>
<a class="rounded-btn" href="/dashboard/pro/org/<%= @user.organization_id %>">Pro Analytics for <%= @user.organization.name %></a>
<% if @user.has_role? :pro %>
<hr>
<a class="rounded-btn" href="/dashboard/pro">Pro Analytics</a>
<% @member_organizations.each do |org| %>
<a class="rounded-btn" href="/dashboard/pro/org/<%= org.id %>"><%= org.name %></a>
<% end %>
<% end %>
</h1>
<% end %>
<% if @org_dashboard %>
<% if params[:which] == "organization" %>
<%= render "analytics" %>
<% @articles.each do |article| %>
<%= render "dashboard_article", article: article, org_admin: true, manage_view: false %>
<%= render "dashboard_article", article: article, organization: article.organization, org_admin: true, manage_view: false %>
<% end %>
<% elsif @articles.any? %>
<%= render "analytics" %>
@ -33,7 +36,7 @@
<% end %>
</div>
<% @articles.each do |article| %>
<%= render "dashboard_article", article: article, org_admin: false, manage_view: false %>
<%= render "dashboard_article", article: article, organization: nil, org_admin: false, manage_view: false %>
<% end %>
<% else %>
<div class="no-articles">

View file

@ -32,15 +32,17 @@
<a class="query-filter-button <%= "selected" if params[:filter].to_s.casecmp("comments").zero? && params[:org_id].blank? %>" href="/notifications/comments">COMMENTS</a>
<a class="query-filter-button <%= "selected" if params[:filter].to_s.casecmp("posts").zero? && params[:org_id].blank? %>" href="/notifications/posts">POSTS</a>
</div>
<% if user_signed_in? && current_user.organization_id.present? %>
<hr>
<header>
<h4><%= current_user.organization.name %> <%= "(#{@total_org_unread})" if @total_org_unread.positive? %></h4>
</header>
<div class="widget-body">
<a class="query-filter-button <%= "selected" if params[:filter].to_s.casecmp("org").zero? && params[:org_id].to_i == current_user.organization_id %>" href="/notifications/org/<%= current_user.organization_id %>">ALL</a>
<a class="query-filter-button <%= "selected" if params[:filter].to_s.casecmp("comments").zero? && params[:org_id].to_i == current_user.organization_id %>" href="/notifications/comments/<%= current_user.organization_id %>">COMMENTS</a>
</div>
<% if @organizations.present? %>
<% @organizations.each do |org| %>
<hr>
<header>
<h4><%= org.name %> <%= "(#{org.notifications.where(read: false).size})" if org.notifications.where(read: false).size.positive? %></h4>
</header>
<div class="widget-body">
<a class="query-filter-button <%= "selected" if params[:filter].to_s.casecmp("org").zero? && params[:org_id].to_i == org.id %>" href="/notifications/org/<%= org.id %>">ALL</a>
<a class="query-filter-button <%= "selected" if params[:filter].to_s.casecmp("comments").zero? && params[:org_id].to_i == org.id %>" href="/notifications/comments/<%= org.id %>">COMMENTS</a>
</div>
<% end %>
<% end %>
</div>
</div>
@ -50,13 +52,15 @@
<a class="query-filter-button <%= "selected" if params[:filter].to_s.casecmp("comments").zero? && params[:org_id].blank? %>" href="/notifications/comments">COMMENTS</a>
<a class="query-filter-button <%= "selected" if params[:filter].to_s.casecmp("posts").zero? && params[:org_id].blank? %>" href="/notifications/posts">POSTS</a>
</div>
<% if current_user.organization_id.present? %>
<div class="notifications-mobile-filters">
<hr>
<span class="organization-name"><%= current_user.organization.name %> <%= "(#{@total_org_unread})" if @total_org_unread.positive? %></span>
<a class="query-filter-button <%= "selected" if params[:filter].to_s.casecmp("org").zero? && params[:org_id].to_i == current_user.organization_id %>" href="/notifications/org/<%= current_user.organization_id %>">ALL</a>
<a class="query-filter-button <%= "selected" if params[:filter].to_s.casecmp("comments").zero? && params[:org_id].to_i == current_user.organization_id %>" href="/notifications/comments/<%= current_user.organization_id %>">COMMENTS</a>
</div>
<% if @organizations.present? %>
<% @organizations.each do |org| %>
<div class="notifications-mobile-filters">
<hr>
<span class="organization-name"><%= org.name %> <%= "(#{org.notifications.where(read: false).size})" if org.notifications.where(read: false).size.positive? %></span>
<a class="query-filter-button <%= "selected" if params[:filter].to_s.casecmp("org").zero? && params[:org_id].to_i == org.id %>" href="/notifications/org/<%= org.id %>">ALL</a>
<a class="query-filter-button <%= "selected" if params[:filter].to_s.casecmp("comments").zero? && params[:org_id].to_i == org.id %>" href="/notifications/comments/<%= org.id %>">COMMENTS</a>
</div>
<% end %>
<% end %>
<%= render "notifications_list" %>
<% if @notifications.any? %>
@ -69,5 +73,3 @@
<% else %>
<%= render "devise/registrations/registration_form" %>
<% end %>

View file

@ -17,27 +17,30 @@
</style>
<div class="settings-org-members">
<h3>Organization Members</h3>
<% @organization.users.each do |user| %>
<div class="settings-org-member-row <%= "settings-org-admin-row" if user.org_admin %>">
<a href="<%= user.path %>"><%= user.name %></a>
<% @org_organization_memberships.each do |org_membership| %>
<div class="settings-org-member-row <%= "settings-org-admin-row" if org_membership.type_of_user == "admin" %>">
<a href="<%= org_membership.user.path %>"><%= org_membership.user.name %></a>
<div class="settings-org-member-actions">
<% if user.org_admin && user == current_user %>
<% if org_membership.user == @user && org_membership.type_of_user == "admin" %>
<button class="settings-org-member settings-org-member-admin-self">you (admin)</button>
<% elsif user.org_admin && user != current_user %>
<% elsif org_membership.user != @user && org_membership.type_of_user == "admin" %>
<button class="settings-org-member settings-org-member-admin-other">admin</button>
<%= form_tag "/users/remove_org_admin", onsubmit: "return confirm('Are you sure you want to take away admin status from #{user.name}?');" do %>
<%= hidden_field_tag "user_id", user.id %>
<button class="settings-org-member settings-org-member-remove" title="Remove from org">revoke admin status</button>
<%= form_tag "/users/remove_org_admin", onsubmit: "return confirm('Are you sure you want to take away admin status from #{org_membership.user.name}?');" do %>
<%= hidden_field_tag "user_id", org_membership.user_id %>
<%= hidden_field_tag "organization_id", @organization.id %>
<button class="settings-org-member settings-org-member-remove" title="Revoke admin status" type="submit">revoke admin status</button>
<% end %>
<% else %>
<button class="settings-org-member settings-org-member-admin-self">org member</button>
<%= form_tag "/users/add_org_admin", onsubmit: "return confirm('Are you sure you want to give #{user.name} admin status?');" do %>
<%= hidden_field_tag "user_id", user.id %>
<button class="settings-org-member settings-org-member-make-admin">make admin</button>
<%= form_tag "/users/add_org_admin", onsubmit: "return confirm('Are you sure you want to give #{org_membership.user.name} admin status?');" do %>
<%= hidden_field_tag "user_id", org_membership.user_id %>
<%= hidden_field_tag "organization_id", @organization.id %>
<button class="settings-org-member settings-org-member-make-admin" title="Make admin" type="submit">make admin</button>
<% end %>
<%= form_tag "/users/remove_from_org", onsubmit: "return confirm('Are you sure you want remove #{user.name} from the organization?');" do %>
<%= hidden_field_tag "user_id", user.id %>
<button class="settings-org-member settings-org-member-remove" title="Remove from org">remove from org</button>
<%= form_tag "/users/remove_from_org", onsubmit: "return confirm('Are you sure you want remove #{org_membership.user.name} from the organization?');" do %>
<%= hidden_field_tag "user_id", org_membership.user_id %>
<%= hidden_field_tag "organization_id", @organization.id %>
<button class="settings-org-member settings-org-member-remove" title="Remove from org" type="submit">remove from org</button>
<% end %>
<% end %>
</div>
@ -56,11 +59,13 @@
<h5>Your secret: (You should rotate this regularly)</h5>
<input type="text" class="settings-org-secret" id="settings-org-secret" value="<%= @organization.secret %>" readonly aria-label="Organization secret (to be rotated regularly)">
<%= form_tag "/organizations/generate_new_secret", onsubmit: "return confirm('Are you sure you want to generate a new secret? All outstanding secrets will be invalid.');" do %>
<%= hidden_field_tag "organization[id]", @organization.id %>
<button class="settings-generate-new-secret">Generate New Secret</button>
<% end %>
</div>
<h3>Organization Details</h3>
<%= form_for(@organization) do |f| %>
<%= f.hidden_field :id %>
<div class="field">
<%= f.label :name %>
<%= f.text_field :name, maxlength: 50 %>

View file

@ -0,0 +1 @@
<%= render "org_member" %>

View file

@ -1,6 +1,6 @@
<div class="settings-org-members">
<h3><%= @user.organization.name %></h3>
<% @user.organization.users.each do |user| %>
<h3><%= @organization.name %></h3>
<% @organization.users.each do |user| %>
<div class="settings-org-member-row">
<a href="<%= user.path %>"><%= user.name %></a>
</div>

View file

@ -1,7 +1,5 @@
<% if @user.organization && @user.org_admin %>
<%= render "users/org_admin" %>
<% elsif @user.organization %>
<%= render "users/org_member" %>
<% if @organization_membership %>
<%= render "users/org_#{@organization_membership.type_of_user}" %>
<% else %>
<%= render "users/org_non_member" %>
<% end %>

View file

@ -1,8 +1,8 @@
<% title "Settings" %>
<% if flash[:notice] && flash[:notice].include?("Your ") %>
<% if flash[:settings_notice] %>
<div class="notice" id="notice">
<%= flash[:notice] %>
<%= flash[:settings_notice] %>
</div>
<% end %>
@ -20,8 +20,21 @@
<% end %>
<div class="user-settings-page">
<% if @tab == "organization" && @user.organization.present? %>
<h2>Settings for <a href="/<%= @user.organization.slug %>">@<%= @user.organization.slug %></a></h2>
<% if @tab == "organization" && @organizations.present? %>
<h2>Settings for
<span class="organization-dropdown">
<select id="organization-dropdown-select" onchange="window.location.href=this.value">
<% options = @organizations.pluck(:slug, :id).each do |arr| %>
<% arr[0] = "@#{arr[0]}" %>
<% arr[1] = "/settings/organization/#{arr[1]}" %>
<% end.to_h.merge("New Organization" => "/settings/organization/new") %>
<%= options_for_select(options, "/settings/organization/#{params[:org_id]}") %>
<%# For example: %>
<%# <option value="/settings/organization/1>@devteam</option> %>
<%# <option value="/settings/organization/new" selected>New Organization</option> %>
</select>
</span>
</h2>
<% else %>
<h2>Settings for <a href="/<%= @user.username %>">@<%= @user.username %></a></h2>
<% end %>

View file

@ -198,7 +198,7 @@ Rails.application.routes.draw do
post "users/update_language_settings" => "users#update_language_settings"
post "users/update_twitch_username" => "users#update_twitch_username"
post "users/join_org" => "users#join_org"
post "users/leave_org" => "users#leave_org"
post "users/leave_org/:organization_id" => "users#leave_org"
post "users/add_org_admin" => "users#add_org_admin"
post "users/remove_org_admin" => "users#remove_org_admin"
post "users/remove_from_org" => "users#remove_from_org"
@ -266,6 +266,7 @@ Rails.application.routes.draw do
end
get "/settings/(:tab)" => "users#edit"
get "/settings/:tab/:org_id" => "users#edit"
get "/signout_confirm" => "users#signout_confirm"
get "/dashboard" => "dashboards#show"
get "/dashboard/pro" => "dashboards#pro"
@ -275,9 +276,9 @@ Rails.application.routes.draw do
constraints: {
which: /organization_user_followers|user_followers/
}
get "/dashboard/:which" => "dashboards#show",
get "/dashboard/:which/:org_id" => "dashboards#show",
constraints: {
which: /organization|reading/
which: /organization/
}
get "/dashboard/:username" => "dashboards#show"

View file

@ -0,0 +1,5 @@
class AddArticlesCountToOrganizations < ActiveRecord::Migration[5.2]
def change
add_column :organizations, :articles_count, :integer, default: 0, null: false
end
end

View file

@ -12,7 +12,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_05_04_131412) do
ActiveRecord::Schema.define(version: 2019_05_21_190118) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -538,6 +538,7 @@ ActiveRecord::Schema.define(version: 2019_05_04_131412) do
create_table "organizations", id: :serial, force: :cascade do |t|
t.string "address"
t.boolean "approved", default: false
t.integer "articles_count", default: 0, null: false
t.string "bg_color_hex"
t.string "city"
t.string "company_size"

View file

@ -1,4 +1,7 @@
FactoryBot.define do
factory :credit do
# schema defaults
# cost 0.0
# spent false
end
end

View file

@ -23,6 +23,14 @@ FactoryBot.define do
signup_cta_variant { "navbar_basic" }
email_digest_periodic { false }
after(:create) do |user|
create(:identity, user_id: user.id)
end
trait :two_identities do
after(:create) { |user| create(:identity, user_id: user.id, provider: "twitter") }
end
trait :super_admin do
after(:build) { |user| user.add_role(:super_admin) }
end
@ -55,28 +63,19 @@ FactoryBot.define do
end
trait :org_member do
after(:build) do |user|
after(:create) do |user|
org = create(:organization)
user.organization_id = org.id
create(:organization_membership, user_id: user.id, organization_id: org.id, type_of_user: "member")
end
end
trait :org_admin do
after(:build) do |user|
after(:create) do |user|
org = create(:organization)
user.organization_id = org.id
user.org_admin = true
create(:organization_membership, user_id: user.id, organization_id: org.id, type_of_user: "admin")
end
end
after(:create) do |user|
create(:identity, user_id: user.id)
end
trait :two_identities do
after(:create) { |user| create(:identity, user_id: user.id, provider: "twitter") }
end
trait :with_article do
after(:create) do |user|
create(:article, user_id: user.id)

View file

@ -3,6 +3,7 @@ require "rails_helper"
RSpec.describe ClassifiedListing, type: :model do
let(:classified_listing) { create(:classified_listing, user_id: user.id) }
let(:user) { create(:user) }
let(:organization) { create(:organization) }
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_presence_of(:body_markdown) }
@ -21,7 +22,7 @@ RSpec.describe ClassifiedListing, type: :model do
end
it "is valid with organization_id" do
cl = build(:classified_listing, user_id: nil, organization_id: create(:organization).id)
cl = build(:classified_listing, user_id: nil, organization_id: organization.id)
expect(cl).to be_valid
end
end

View file

@ -1,15 +1,39 @@
require "rails_helper"
RSpec.describe Credit, type: :model do
it "counts credits for user" do
user = create(:user)
Credit.create(user_id: user.id)
Credit.create(user_id: user.id)
expect(user.reload.credits_count).to eq(2)
let(:user) { create(:user) }
let(:organization) { create(:organization) }
let(:random_number) { rand(100) }
xit "counts credits for user" do
# See https://github.com/magnusvk/counter_culture/issues/259
create_list(:credit, random_number, user: user)
expect(user.reload.credits_count).to eq(random_number)
end
it "counts credits for organization" do
organization = create(:organization)
Credit.create(organization_id: organization.id)
expect(organization.reload.credits_count).to eq(1)
xit "counts credits for organization" do
# See https://github.com/magnusvk/counter_culture/issues/259
create_list(:credit, random_number, organization: organization)
expect(organization.reload.credits_count).to eq(random_number)
end
it "counts the number of unspent credits for a user" do
create_list(:credit, random_number, user: user)
expect(user.reload.unspent_credits_count).to eq(random_number)
end
it "counts the number of spent credits for a user" do
create_list(:credit, random_number, user: user, spent: true)
expect(user.reload.spent_credits_count).to eq(random_number)
end
it "counts the number of unspent credits for an organization" do
create_list(:credit, random_number, organization: organization)
expect(organization.reload.unspent_credits_count).to eq(random_number)
end
it "counts the number of spent credits for an organization" do
create_list(:credit, random_number, organization: organization, spent: true)
expect(organization.reload.spent_credits_count).to eq(random_number)
end
end

View file

@ -111,7 +111,7 @@ RSpec.describe Notification, type: :model do
it "sends a notification to the organization" do
org = create(:organization)
user.update(organization: org)
create(:organization_membership, user: user, organization: org)
article.update(organization: org)
comment = create(:comment, user: user2, commentable: article)
Notification.send_new_comment_notifications_without_delay(comment)
@ -231,7 +231,7 @@ RSpec.describe Notification, type: :model do
let(:org) { create(:organization) }
before do
user.update(organization: org, org_admin: true)
create(:organization_membership, user: user, organization: org, type_of_user: "admin")
article.update(organization: org)
end

View file

@ -2,7 +2,7 @@ require "rails_helper"
RSpec.describe OrganizationMembership, type: :model do
describe "validations" do
subject { create(:organization_membership) }
subject { build(:organization_membership) }
it { is_expected.to validate_presence_of(:user_id) }
it { is_expected.to validate_presence_of(:organization_id) }

View file

@ -643,12 +643,12 @@ RSpec.describe User, type: :model do
describe "organization admin privileges" do
it "recognizes an org admin" do
user.update(organization: org, org_admin: true)
create(:organization_membership, user: user, organization: org, type_of_user: "admin")
expect(user.org_admin?(org)).to be true
end
it "forbids an incorrect org admin" do
user.update(organization: org, org_admin: true)
create(:organization_membership, user: user, organization: org, type_of_user: "admin")
expect(user.org_admin?(second_org)).to be false
expect(second_user.org_admin?(org)).to be false
end

View file

@ -25,9 +25,12 @@ RSpec.describe OrganizationPolicy do
end
context "when user is an org admin of an org" do
let(:user) { build(:user) }
subject(:organization_policy) { described_class.new(user, org) }
before { user.update(organization: organization, org_admin: true) }
let(:user) { create(:user) }
let(:org) { create(:organization) }
before { create(:organization_membership, user: user, organization: org, type_of_user: "admin") }
it "allows the user to update their own org" do
expect(organization_policy).to permit_action(:update)
@ -35,10 +38,13 @@ RSpec.describe OrganizationPolicy do
end
context "when user is an org admin of another org" do
let(:user) { build(:user) }
let(:new_org) { build(:organization) }
subject(:organization_policy) { described_class.new(user, new_org) }
before { user.update(organization: new_org, org_admin: true) }
let(:user) { create(:user) }
let(:org) { create(:organization) }
let(:new_org) { create(:organization) }
before { create(:organization_membership, user: user, organization: org, type_of_user: "admin") }
it "does not allow the user to update another org" do
expect(organization_policy).to forbid_action(:update)

View file

@ -15,7 +15,7 @@ RSpec.describe UserPolicy do
let(:user) { other_user }
permitted_actions = %i[
edit update onboarding_update join_org leave_org dashboard_show remove_association destroy
edit update onboarding_update join_org dashboard_show remove_association destroy
]
it { is_expected.to permit_actions(permitted_actions) }
@ -27,36 +27,6 @@ RSpec.describe UserPolicy do
end
end
context "when user is org_admin" do
let(:org) { build(:organization) }
let(:other_org) { build(:organization) }
let(:user) { build(:user, org_admin: true, organization: org) }
context "with other_user as org_member of same org" do
let(:other_user) { build(:user, organization: org) }
it { is_expected.to permit_actions(%i[add_org_admin remove_from_org]) }
end
context "with other_user as org_member of a different org" do
let(:other_user) { build(:user, organization: other_org) }
it { is_expected.to forbid_actions(%i[add_org_admin remove_from_org]) }
end
context "with other_user as org admin" do
let(:other_user) { build(:user, org_admin: true, organization: org) }
it { is_expected.to permit_actions(%i[remove_org_admin]) }
end
context "with other_user as org adming of a different org" do
let(:other_user) { build(:user, org_admin: true, organization: other_org) }
it { is_expected.to forbid_actions(%i[remove_org_admin]) }
end
end
context "when user is trusted" do
let(:user) { build(:user, :trusted) }

View file

@ -258,11 +258,11 @@ RSpec.describe "Api::V0::Articles", type: :request do
end
it "creates an article on behalf of an organization" do
user.update_columns(organization_id: organization.id)
create(:organization_membership, user: user, organization: organization)
expect do
post_article(
title: Faker::Book.title + rand(100).to_s,
publish_under_org: true,
organization_id: organization.id,
)
expect(response).to have_http_status(:created)
end.to change(Article, :count).by(1)
@ -593,8 +593,8 @@ RSpec.describe "Api::V0::Articles", type: :request do
it "assigns the article to the organization" do
expect(article.organization).to be_nil
user.update_columns(organization_id: organization.id)
put_article(publish_under_org: true)
create(:organization_membership, user: user, organization: organization)
put_article(organization_id: organization.id)
expect(response).to have_http_status(:ok)
expect(article.reload.organization).to eq(organization)
end

View file

@ -1,8 +1,7 @@
require "rails_helper"
RSpec.describe "ArticlesCreate", type: :request do
let(:organization) { create(:organization) }
let(:user) { create(:user, organization_id: organization.id) }
let(:user) { create(:user, :org_member) }
before do
sign_in user
@ -27,14 +26,15 @@ RSpec.describe "ArticlesCreate", type: :request do
end
it "creates article with front matter params and org" do
user_org_id = user.organizations.first.id
post "/articles", params: {
article: {
body_markdown: "---\ntitle: hey hey hahuu\npublished: false\n---\nYo ho ho#{rand(100)}",
tag_list: "yo",
post_under_org: true
organization_id: user_org_id
}
}
expect(Article.last.organization_id).to eq(organization.id)
expect(Article.last.organization_id).to eq(user_org_id)
end
it "creates series when series is created with frontmatter" do

View file

@ -3,7 +3,7 @@ require "rails_helper"
RSpec.describe "ArticlesUpdate", type: :request do
let(:organization) { create(:organization) }
let(:organization2) { create(:organization) }
let(:user) { create(:user, organization_id: organization.id) }
let(:user) { create(:user, :org_admin) }
let(:user2) { create(:user, organization_id: organization2.id) }
let(:article) { create(:article, user_id: user.id) }
@ -32,16 +32,18 @@ RSpec.describe "ArticlesUpdate", type: :request do
end
it "adds organization ID when user updates" do
user_org_id = user.organizations.first.id
put "/articles/#{article.id}", params: {
article: { post_under_org: true }
article: { organization_id: user_org_id }
}
expect(article.reload.organization_id).to eq organization.id
expect(article.reload.organization_id).to eq user_org_id
end
it "removes organization ID when user updates" do
article.update_column(:organization_id, organization.id)
article.update_column(:organization_id, user.organizations.first.id)
put "/articles/#{article.id}", params: {
article: { post_under_org: false }
# use empty string instead of nil to mock article form submission
article: { organization_id: "" }
}
expect(article.reload.organization_id).to eq nil
end
@ -64,12 +66,13 @@ RSpec.describe "ArticlesUpdate", type: :request do
end
it "allows an org admin to assign an org article to another user" do
user.update_columns(org_admin: true)
article.update_columns(organization_id: user.organization_id)
other_user = create(:user, organization: user.organization)
admin_org_id = user.organizations.first.id
article.update_columns(organization_id: admin_org_id)
other_user = create(:user)
create(:organization_membership, user_id: other_user.id, organization_id: admin_org_id)
put "/articles/#{article.id}", params: { article: { user_id: other_user.id } }
expect(article.reload.user).to eq(other_user)
expect(article.organization).to eq(user.organization)
expect(article.organization_id).to eq(admin_org_id)
end
end

View file

@ -8,18 +8,72 @@ RSpec.describe "ClassifiedListings", type: :request do
title: "something",
body_markdown: "something else",
category: "cfp",
tag_list: "",
post_as_organization: "0"
tag_list: ""
}
}
end
before do
sign_in user
create_list(:credit, 25, user: user)
describe "GET /listings/new" do
before { sign_in user }
context "when the user has no credits" do
it "shows the proper messages" do
get "/listings/new"
expect(response.body).to include "Listings Require Credits"
expect(response.body).to include "You need at least one credit to create a listing."
end
end
context "when the user has credits" do
it "shows the number of credits" do
random_number = rand(2..100)
create_list(:credit, random_number, user: user)
get "/listings/new"
expect(response.body).to include "You have #{random_number} credits available"
end
end
context "when the user has no credits and belongs to an organization" do
let(:organization) { create(:organization) }
before { create(:organization_membership, user_id: user.id, organization_id: organization.id) }
it "shows the proper message when both user and org have no credits" do
get "/listings/new"
expect(response.body).to include "Listings Require Credits"
end
it "shows the number of credits of the user if the user has credits but the org has no credits" do
random_number = rand(2..100)
create_list(:credit, random_number, user: user)
get "/listings/new"
expect(response.body).to include "You have #{random_number} credits available"
end
it "shows the number of credits of the organization if the org has credits" do
random_number = rand(2..100)
create_list(:credit, random_number, organization: organization)
get "/listings/new"
expect(response.body).to include "has <span id=\"org-credits-number\">#{random_number}</span> credits"
end
it "shows the number of credits of both the user and the organization if they both have credits" do
random_number = rand(2..100)
create_list(:credit, random_number, organization: organization)
create_list(:credit, random_number, user: user)
get "/listings/new"
expect(response.body).to include "has <span id=\"org-credits-number\">#{random_number}</span> credits"
expect(response.body).to include "You have #{random_number} credits available"
end
end
end
describe "POST /listings" do
before do
sign_in user
create_list(:credit, 25, user: user)
end
context "when the listing is invalid" do
it "renders errors with the listing" do
post "/listings", params: {
@ -27,8 +81,7 @@ RSpec.describe "ClassifiedListings", type: :request do
title: "nothing",
body_markdown: "",
category: "cfp",
tag_list: "",
post_as_organization: "0"
tag_list: ""
}
}
expect(response.body).to include("prohibited this listing from being saved")
@ -62,11 +115,12 @@ RSpec.describe "ClassifiedListings", type: :request do
it "creates a listing under the org" do
org_admin = create(:user, :org_admin)
Credit.create(organization_id: org_admin.organization_id)
valid_listing_params[:classified_listing][:post_as_organization] = "1"
org_id = org_admin.organizations.first.id
Credit.create(organization_id: org_id)
valid_listing_params[:classified_listing][:organization_id] = org_id
sign_in org_admin
post "/listings", params: valid_listing_params
expect(ClassifiedListing.first.organization_id).to eq org_admin.organization_id
expect(ClassifiedListing.first.organization_id).to eq org_id
end
end
end

View file

@ -3,32 +3,35 @@ require "rails_helper"
RSpec.describe "Credits", type: :request do
describe "GET /credits" do
let(:user) { create(:user) }
let(:organization) { create(:organization) }
before do
sign_in user
end
let(:org_member) { create(:user, :org_member) }
let(:org_admin) { create(:user, :org_admin) }
it "shows credits page" do
sign_in user
get "/credits"
expect(response.body).to include("You have")
end
it "shows credits page if user belongs to an org" do
user.update_column(:organization_id, organization.id)
org = org_member.organizations.first
sign_in org_member
get "/credits"
expect(response.body).to include("You have")
expect(response.body).not_to include(CGI.escapeHTML(org.name))
end
it "shows credits page if user belongs to an org and is org admin" do
user.update_columns(organization_id: organization.id, org_admin: true)
org = org_admin.organizations.first
sign_in org_admin
get "/credits"
expect(response.body).to include(CGI.escapeHTML(organization.name))
expect(response.body).to include(CGI.escapeHTML(org.name))
end
end
describe "POST credits" do
let(:user) { create(:user) }
let(:org_admin) { create(:user, :org_admin) }
let(:admin_org_id) { org_admin.organizations.first.id }
let(:stripe_helper) { StripeMock.create_test_helper }
before do
@ -104,24 +107,22 @@ RSpec.describe "Credits", type: :request do
end
context "when purchasing as an organization" do
let(:org_admin) { create(:user, :org_admin) }
before { sign_in org_admin }
it "creates unspent credits for the organization" do
post "/credits", params: {
user_type: "organization",
organization_id: admin_org_id,
credit: {
number_to_purchase: 20
},
stripe_token: stripe_helper.generate_card_token
}
expect(Credit.where(organization_id: org_admin.organization_id, spent: false).size).to eq 20
expect(Credit.where(organization_id: admin_org_id, spent: false).size).to eq 20
end
it "makes a valid Stripe charge" do
post "/credits", params: {
user_type: "organization",
organization_id: admin_org_id,
credit: {
number_to_purchase: 20
},
@ -133,7 +134,7 @@ RSpec.describe "Credits", type: :request do
it "does not create unspent credits for the current_user" do
post "/credits", params: {
user_type: "organization",
organization_id: admin_org_id,
credit: {
number_to_purchase: 20
},
@ -154,6 +155,19 @@ RSpec.describe "Credits", type: :request do
}
expect(user.credits.where(spent: false).size).to eq(0)
end
it "does not reward credits for orgs" do
sign_in org_admin
StripeMock.prepare_error(Stripe::CardError.new(2, 3, 4), :new_charge)
post "/credits", params: {
organization_id: admin_org_id,
credit: {
number_to_purchase: 25
},
stripe_token: stripe_helper.generate_card_token
}
expect(Credit.where(organization_id: admin_org_id, spent: false).size).to eq(0)
end
end
end
end

View file

@ -3,7 +3,6 @@ require "rails_helper"
RSpec.describe "Dashboards", type: :request do
let(:user) { create(:user) }
let(:second_user) { create(:user) }
let(:org_admin) { create(:user, :org_admin) }
let(:super_admin) { create(:user, :super_admin) }
let(:article) { create(:article, user_id: user.id) }
@ -47,7 +46,7 @@ RSpec.describe "Dashboards", type: :request do
context "when logged in" do
it "renders user's organization articles" do
user.update(organization_id: organization.id, org_admin: true)
create(:organization_membership, user: user, organization: organization, type_of_user: "admin")
article.update(organization_id: organization.id)
sign_in user
get "/dashboard/organization"
@ -164,9 +163,11 @@ RSpec.describe "Dashboards", type: :request do
context "when user has pro permission and is an org admin" do
it "shows page properly" do
org_admin.add_role(:pro)
sign_in org_admin
get "/dashboard/pro/org/#{org_admin.organization_id}"
org = create :organization
create(:organization_membership, user: user, organization: org, type_of_user: "admin")
user.add_role(:pro)
login_as user
get "/dashboard/pro/org/#{org.id}"
expect(response.body).to include("pro")
end
end
@ -174,7 +175,7 @@ RSpec.describe "Dashboards", type: :request do
context "when user has pro permission and is an org member" do
it "shows page properly" do
org = create :organization
user.update(organization_id: org.id)
create(:organization_membership, user: user, organization: org)
user.add_role(:pro)
sign_in user
get "/dashboard/pro/org/#{org.id}"

View file

@ -29,40 +29,60 @@ RSpec.describe "/listings", type: :request do
end
it "creates proper listing if credits are available" do
post "/listings", params: { classified_listing: {
title: "Hey", category: "education", body_markdown: "hey hey my my"
} }
post "/listings", params: {
classified_listing: { title: "Hey", category: "education", body_markdown: "hey hey my my" }
}
expect(ClassifiedListing.last.processed_html).to include("hey my")
end
it "spends credits" do
num_credits = Credit.where(spent: true).size
post "/listings", params: { classified_listing: {
title: "Hey", category: "education", body_markdown: "hey hey my my"
} }
post "/listings", params: {
classified_listing: { title: "Hey", category: "education", body_markdown: "hey hey my my" }
}
expect(Credit.where(spent: true).size).to be > num_credits
end
it "adds tags" do
post "/listings", params: { classified_listing: {
title: "Hey", category: "education", body_markdown: "hey hey my my", tag_list: "ruby, rails, go"
} }
post "/listings", params: {
classified_listing: { title: "Hey", category: "education", body_markdown: "hey hey my my", tag_list: "ruby, rails, go" }
}
expect(ClassifiedListing.last.cached_tag_list).to include("rails")
end
it "creates the listing for the user" do
user.update_column(:organization_id, organization.id)
post "/listings", params: { classified_listing: {
title: "Hey", category: "education", body_markdown: "hey hey my my", tag_list: "ruby, rails, go",
post_as_organization: 0
} }
expect(ClassifiedListing.last.organization_id).not_to eq(organization.id)
it "creates the listing under the user" do
post "/listings", params: {
classified_listing: { title: "Hey", category: "education", body_markdown: "hey hey my my", tag_list: "ruby, rails, go" }
}
expect(ClassifiedListing.last.user_id).to eq user.id
end
it "creates the listing for the user if no organization_id is selected" do
create(:organization_membership, user_id: user.id, organization_id: organization.id)
post "/listings", params: {
classified_listing: { title: "Hey", category: "education", body_markdown: "hey hey my my", tag_list: "ruby, rails, go" }
}
expect(ClassifiedListing.last.organization_id).to eq nil
expect(ClassifiedListing.last.user_id).to eq user.id
end
it "creates the listing for the organization" do
user.update_column(:organization_id, organization.id)
post "/listings", params: { classified_listing: {
title: "Hey", category: "education", body_markdown: "hey hey my my", tag_list: "ruby, rails, go",
post_as_organization: 1
} }
create(:organization_membership, user_id: user.id, organization_id: organization.id)
post "/listings", params: {
classified_listing: { title: "Hey", category: "education", body_markdown: "hey hey my my", tag_list: "ruby, rails, go", organization_id: organization.id }
}
expect(ClassifiedListing.last.organization_id).to eq(organization.id)
end
it "does not create an org listing if a different org member doesn't belong to the org" do
another_org = create(:organization)
create(:organization_membership, user_id: user.id, organization_id: another_org.id)
expect do
post "/listings", params: {
classified_listing: { title: "Hey", category: "education", body_markdown: "hey hey my my", tag_list: "ruby, rails, go", organization_id: organization.id }
}
end.to raise_error Pundit::NotAuthorizedError
end
end
describe "GETS /listings/edit" do
@ -88,24 +108,26 @@ RSpec.describe "/listings", type: :request do
end
it "updates bumped_at if action is bump" do
# block = create(:block, user_id: user.id, input_css: ".blue { color: blue;}")
put "/listings/#{classified_listing.id}", params: {
classified_listing: { action: "bump" }
}
expect(ClassifiedListing.last.bumped_at).to be > 10.seconds.ago
end
it "updates publish if action is unpublish" do
put "/listings/#{classified_listing.id}", params: {
classified_listing: { action: "unpublish" }
}
expect(ClassifiedListing.last.published).to eq(false)
end
it "updates body_markdown" do
put "/listings/#{classified_listing.id}", params: {
classified_listing: { body_markdown: "hello new markdown" }
}
expect(ClassifiedListing.last.body_markdown).to eq("hello new markdown")
end
it "does not update body_markdown if not bumped/created recently" do
classified_listing.update_column(:bumped_at, 50.hours.ago)
put "/listings/#{classified_listing.id}", params: {
@ -113,6 +135,7 @@ RSpec.describe "/listings", type: :request do
}
expect(ClassifiedListing.last.body_markdown).not_to eq("hello new markdown")
end
it "updates other fields" do
put "/listings/#{classified_listing.id}", params: {
classified_listing: { body_markdown: "hello new markdown", title: "New title!", tag_list: "new, tags, hey" }
@ -120,6 +143,7 @@ RSpec.describe "/listings", type: :request do
expect(ClassifiedListing.last.title).to eq("New title!")
expect(ClassifiedListing.last.cached_tag_list).to include("hey")
end
it "does not update other fields" do
classified_listing.update_column(:bumped_at, 50.hours.ago)
put "/listings/#{classified_listing.id}", params: {

View file

@ -17,9 +17,11 @@ RSpec.describe "Notifications::Reads", type: :request do
expect(stubbed_service_object).to have_received(:mark_as_read).once
end
it "marks personal and org Notifications as read" do
allow(user).to receive(:organization_id).and_return(1)
post "/notifications/reads/", params: { org_id: 1 }
it "marks personal and org notifications as read" do
org_admin = create(:user, :org_admin)
org_id = org_admin.organizations.first.id
sign_in org_admin
post "/notifications/reads/", params: { org_id: org_id }
expect(response).to have_http_status(:ok)
expect(stubbed_service_object).to have_received(:mark_as_read).twice
end

View file

@ -1,32 +1,33 @@
require "rails_helper"
RSpec.describe "OrganizationsUpdate", type: :request do
let(:organization) { create(:organization) }
let(:user) { create(:user, organization_id: organization.id) }
let(:user) { create(:user, :org_admin) }
let(:org_id) { user.organizations.first.id }
let(:article) { create(:article, user_id: user.id) }
let(:comment) { create(:comment, user_id: user.id, commentable_id: article.id) }
before do
user.org_admin = true
sign_in user
end
it "updates org color with proper params" do
put "/organizations/#{organization.id}", params: {
organization: { text_color_hex: "#111111" }
put "/organizations/#{org_id}", params: {
organization: { id: org_id, text_color_hex: "#111111" }
}
expect(Organization.last.text_color_hex).to eq("#111111")
end
it "generates new secret" do
secret = Organization.last.secret
post "/organizations/generate_new_secret"
post "/organizations/generate_new_secret", params: {
organization: { id: org_id }
}
expect(Organization.last.secret).not_to eq(secret)
end
it "updates profile_updated_at" do
organization.update_column(:profile_updated_at, 2.weeks.ago)
put "/organizations/#{organization.id}", params: { organization: { text_color_hex: "#111111" } }
expect(organization.reload.profile_updated_at).to be > 2.minutes.ago
Organization.last.update_column(:profile_updated_at, 2.weeks.ago)
put "/organizations/#{org_id}", params: { organization: { id: org_id, text_color_hex: "#111111" } }
expect(Organization.last.profile_updated_at).to be > 2.minutes.ago
end
end

View file

@ -7,17 +7,12 @@ RSpec.describe "UserOrganization", type: :request do
context "when joining an org" do
before { sign_in user }
it "joins org with proper secret" do
post "/users/join_org", params: { org_secret: organization.secret }
expect(user.organization_id).to eq(organization.id)
end
it "creates an organization_membership association" do
post "/users/join_org", params: { org_secret: organization.secret }
org_membership = OrganizationMembership.first
expect(org_membership.persisted?).to eq true
expect(org_membership.user_id).to eq user.id
expect(org_membership.organization_id).to eq organization.id
expect(org_membership.user).to eq user
expect(org_membership.organization).to eq organization
expect(org_membership.type_of_user).to eq "member"
end
@ -28,7 +23,29 @@ RSpec.describe "UserOrganization", type: :request do
it "correctly strips the secret of the org_secret param" do
post "/users/join_org", params: { org_secret: organization.secret + " " }
expect(user.organization_id).to eq(organization.id)
expect(OrganizationMembership.exists?(user: user, organization: organization)).to eq true
end
end
context "when creating a new org" do
before do
sign_in user
org_params = build(:organization).attributes
org_params["profile_image"] = Rack::Test::UploadedFile.new(Rails.root.join("app", "assets", "images", "android-icon-36x36.png"), "image/jpeg")
post "/organizations", params: { organization: org_params }
end
it "creates the correct organization_membership association" do
org_membership = OrganizationMembership.first
expect(org_membership.persisted?).to eq true
expect(org_membership.user).to eq user
expect(org_membership.organization).to eq Organization.last
expect(org_membership.type_of_user).to eq "admin"
end
it "redirects to the proper org settings page" do
expect(response.status).to eq 302
expect(response.redirect_url).to include "/settings/organization/#{Organization.last.id}"
end
end
@ -37,71 +54,73 @@ RSpec.describe "UserOrganization", type: :request do
before { sign_in org_member }
it "leaves org" do
post "/users/leave_org"
expect(org_member.organization_id).to eq(nil)
end
it "deletes the org_membership association" do
create(:organization_membership, user_id: org_member.id, organization_id: org_member.organization_id)
post "/users/leave_org"
expect(OrganizationMembership.count).to eq 0
it "leaves org and deletes the member's organization membership" do
org_id = org_member.organizations.first.id
post "/users/leave_org/#{org_id}"
expect(OrganizationMembership.exists?(user_id: org_member.id, organization_id: org_id)).to eq false
end
end
context "when adding an org admin" do
let(:org_admin) { create(:user, :org_admin) }
let(:user2) { create(:user, organization_id: org_admin.organization_id) }
let(:org_member) { create(:user, :org_member) }
let(:org_id) { org_admin.organizations.first.id }
let(:user2) { create(:user) }
def add_org_admin
org_admin
create(:organization_membership, user_id: user2.id, organization_id: org_id)
sign_in org_admin
post "/users/add_org_admin", params: { user_id: user2.id }
post "/users/add_org_admin", params: { user_id: user2.id, organization_id: org_id }
end
it "adds org admin" do
org = org_admin.organizations.first
add_org_admin
expect(User.last.org_admin).to eq(true)
expect(user2.org_admin?(org)).to eq(true)
end
it "creates the org_membership association" do
add_org_admin
org_membership = OrganizationMembership.first
org_membership = OrganizationMembership.last
expect(org_membership.persisted?).to eq true
expect(org_membership.user_id).to eq user2.id
expect(org_membership.organization_id).to eq org_admin.organization_id
expect(org_membership.organization_id).to eq org_id
expect(org_membership.type_of_user).to eq "admin"
end
it "raises if user not org_admin" do
user.update(organization_id: organization.id)
expect { post "/users/add_org_admin", params: { user_id: user2.id } }.
it "raises not_authorized if user is not org_admin" do
org_member_org_id = org_member.organizations.first.id
sign_in org_member
expect { post "/users/add_org_admin", params: { user_id: user2.id, organization_id: org_member_org_id } }.
to raise_error Pundit::NotAuthorizedError
end
end
context "when removing an org admin" do
let(:org_admin) { create(:user, :org_admin) }
let(:org_id) { org_admin.organizations.first.id }
let(:second_org_admin) { create(:user, :org_admin) }
before { sign_in org_admin }
before do
second_org_admin.organization_memberships.update_all(organization_id: org_id)
sign_in org_admin
end
it "removes org admin" do
user2 = create(:user, organization_id: org_admin.organization_id, org_admin: true)
post "/users/remove_org_admin", params: { user_id: user2.id }
expect(User.last.org_admin).to eq(false)
post "/users/remove_org_admin", params: { user_id: second_org_admin.id, organization_id: org_id }
expect(second_org_admin.org_admin?(org_id)).to eq false
end
it "updates the correct org_membership association to a member level" do
user2 = create(:user, organization_id: org_admin.organization_id, org_admin: true)
org_membership = create(:organization_membership, organization_id: user2.organization_id, user_id: user2.id, type_of_user: "admin")
post "/users/remove_org_admin", params: { user_id: user2.id }
org_membership = second_org_admin.organization_memberships.first
post "/users/remove_org_admin", params: { user_id: second_org_admin.id, organization_id: org_id }
expect(org_membership.reload.type_of_user).to eq "member"
end
it "remove_org_admin raises if user not org_admin" do
user.update(organization_id: organization.id)
user2 = create(:user, organization_id: organization.id, org_admin: true)
expect { post "/users/remove_org_admin", params: { user_id: user2.id } }.
org_admin.organization_memberships.update_all(type_of_user: "member")
expect { post "/users/remove_org_admin", params: { user_id: second_org_admin.id, organization_id: org_id } }.
to raise_error Pundit::NotAuthorizedError
end
end

View file

@ -31,7 +31,7 @@ RSpec.describe "UserProfiles", type: :request do
end
it "renders organization users on sidebar" do
user.update(organization_id: organization.id)
create(:organization_membership, user_id: user.id, organization_id: organization.id)
get organization.path
expect(response.body).to include user.profile_image_url
end

View file

@ -15,7 +15,7 @@ RSpec.describe "UserSettings", type: :request do
before { sign_in user }
it "renders various settings tabs properly" do
%w[organization switch-organizations misc account].each do |tab|
%w[organization misc account].each do |tab|
get "/settings/#{tab}"
expect(response.body).to include("Settings for")
end
@ -72,7 +72,7 @@ RSpec.describe "UserSettings", type: :request do
it "displays a flash with a reminder for the user to expect an email" do
send_request
expect(flash[:notice]).to include("The export will be emailed to you shortly.")
expect(flash[:settings_notice]).to include("The export will be emailed to you shortly.")
end
it "hides the checkbox" do
@ -200,7 +200,7 @@ RSpec.describe "UserSettings", type: :request do
it "renders a successful response message" do
delete "/users/remove_association", params: { provider: "twitter" }
expect(flash[:notice]).to eq "Your Twitter account was successfully removed."
expect(flash[:settings_notice]).to eq "Your Twitter account was successfully removed."
end
it "does not show the Remove OAuth section afterward" do

View file

@ -15,13 +15,34 @@ RSpec.describe "Organization setting page(/settings/organization)", type: :syste
expect(page).to have_text("Your organization was successfully created and you are an admin.")
end
it "promotes a member to an admin" do
create(:organization_membership, user_id: user.id, organization_id: organization.id, type_of_user: "admin")
user2 = create(:user, username: "newuser")
create(:organization_membership, user_id: user2.id, organization_id: organization.id)
visit "settings/organization"
click_button("make admin")
page.driver.browser.switch_to.alert.accept
expect(page).to have_text("#{user2.name} is now an admin.")
end
it "revokes an admin's privileges" do
create(:organization_membership, user_id: user.id, organization_id: organization.id, type_of_user: "admin")
user2 = create(:user, username: "newuser")
create(:organization_membership, user_id: user2.id, organization_id: organization.id, type_of_user: "admin")
visit "settings/organization"
click_button("revoke admin status")
page.driver.browser.switch_to.alert.accept
expect(page).to have_text("#{user2.name} is no longer an admin.")
end
it "remove user from organization" do
user.update(organization_id: organization.id, org_admin: true)
user2 = create(:user, username: "newuser", organization_id: organization.id)
create(:organization_membership, user_id: user.id, organization_id: organization.id, type_of_user: "admin")
user2 = create(:user, username: "newuser")
create(:organization_membership, user_id: user2.id, organization_id: organization.id)
visit "settings/organization"
click_button("Remove from org")
page.driver.browser.switch_to.alert.accept
expect(page).not_to have_text(user2.name)
expect(page).to have_text("#{user2.name} is no longer part of your organization.")
end
def fill_in_org_form

View file

@ -1,8 +1,8 @@
require "rails_helper"
RSpec.describe "Organization index", type: :system do
let!(:organization) { create(:organization) }
let!(:org_user) { create(:user, organization: organization) }
let!(:org_user) { create(:user, :org_member) }
let(:organization) { org_user.organizations.first }
before do
create_list(:article, 2, organization: organization)

View file

@ -28,7 +28,7 @@ RSpec.describe "User edits their integrations", type: :system, js: true do
fill_in "Twitch Username", with: "TestTwitchUser"
click_button("SUBMIT")
expect(page).to have_content "Your profile was successfully updated."
expect(page).to have_content "Your Twitch username was successfully updated."
visit "/settings/integrations"
expect(find_field("Twitch Username").value).to eq "TestTwitchUser"