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:
parent
c2d3384f80
commit
47d9ec27fb
81 changed files with 882 additions and 554 deletions
|
|
@ -284,6 +284,9 @@
|
|||
.listings-current-credits-inner {
|
||||
display: inline-block;
|
||||
margin: 20px;
|
||||
select {
|
||||
width: initial;
|
||||
}
|
||||
}
|
||||
a {
|
||||
text-decoration: underline;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
{' '}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
35
app/javascript/listings/elements/orgSettings.jsx
Normal file
35
app/javascript/listings/elements/orgSettings.jsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
15
app/javascript/packs/orgCreditsSelector.js
Normal file
15
app/javascript/packs/orgCreditsSelector.js
Normal 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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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] }
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ class Role < ApplicationRecord
|
|||
trusted
|
||||
banned
|
||||
warned
|
||||
switch_between_orgs
|
||||
triple_unicorn_member
|
||||
level_4_member
|
||||
level_3_member
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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" %>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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 🙏
|
||||
|
|
|
|||
|
|
@ -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" %>
|
||||
|
|
|
|||
|
|
@ -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">< 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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
1
app/views/users/_org_guest.html.erb
Normal file
1
app/views/users/_org_guest.html.erb
Normal file
|
|
@ -0,0 +1 @@
|
|||
<%= render "org_member" %>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
class AddArticlesCountToOrganizations < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :organizations, :articles_count, :integer, default: 0, null: false
|
||||
end
|
||||
end
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
FactoryBot.define do
|
||||
factory :credit do
|
||||
# schema defaults
|
||||
# cost 0.0
|
||||
# spent false
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue