Skip to content

organisation user pins#953

Draft
raphael-goetz wants to merge 5 commits intomainfrom
#556-organisation-user-pins
Draft

organisation user pins#953
raphael-goetz wants to merge 5 commits intomainfrom
#556-organisation-user-pins

Conversation

@raphael-goetz
Copy link
Copy Markdown
Member

Resolves: #556

@raphael-goetz raphael-goetz self-assigned this May 4, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

GitLab Pipeline Action

General information

Link to pipeline: https://gitlab.com/code0-tech/development/sagittarius/-/pipelines/2499021059

Status: Passed
Duration: 6 minutes

Job summaries

rspec: [cloud]

Coverage report available at https://code0-tech.gitlab.io/-/development/sagittarius/-/jobs/14206894193/artifacts/tmp/coverage/index.html
Test summary available at https://gitlab.com/code0-tech/development/sagittarius/-/pipelines/2499021059/test_report
Finished in 22.85 seconds (files took 13.25 seconds to load)
1403 examples, 0 failures
Line Coverage: 92.56% (4565 / 4932)
[TEST PROF INFO] Time spent in factories: 00:12.395 (42.53% of total time)

rspec: [ee]

Coverage report available at https://code0-tech.gitlab.io/-/development/sagittarius/-/jobs/14206894192/artifacts/tmp/coverage/index.html
Test summary available at https://gitlab.com/code0-tech/development/sagittarius/-/pipelines/2499021059/test_report
Finished in 24.84 seconds (files took 12.26 seconds to load)
1381 examples, 0 failures
Line Coverage: 92.92% (4465 / 4805)
[TEST PROF INFO] Time spent in factories: 00:13.294 (42.19% of total time)

rspec: [ce]

Coverage report available at https://code0-tech.gitlab.io/-/development/sagittarius/-/jobs/14206894191/artifacts/tmp/coverage/index.html
Test summary available at https://gitlab.com/code0-tech/development/sagittarius/-/pipelines/2499021059/test_report
Finished in 21.78 seconds (files took 14.22 seconds to load)
1331 examples, 0 failures
Line Coverage: 92.44% (4294 / 4645)
[TEST PROF INFO] Time spent in factories: 00:12.204 (39.24% of total time)

docs:preview

Documentation preview available at https://code0-tech.gitlab.io/-/development/telescopium/-/jobs/14206929445/artifacts/out/index.html

rubocop

788 files inspected, no offenses detected

[Types::GlobalIdType[::Organization]],
required: true,
description: 'Ordered list of organization IDs to pin for the user.'
argument :user_id, Types::GlobalIdType[::User], required: true, description: 'ID of the user to update.'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have used the current user rather than passing it as an argument. Or do you see a use case for an user updating the pins of another user?


authorize :read_user_organization_pin

field :organization, Types::OrganizationType, null: false, description: 'The pinned organization'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a user isn't allowed to see the organization (maybe they got removed from it after pinning it), this field will be null due to authorization. If the field couldn't be null, this will bubble up to the first nullable field and return an error.

Suggested change
field :organization, Types::OrganizationType, null: false, description: 'The pinned organization'
field :organization, Types::OrganizationType, null: true, description: 'The pinned organization'

end

def pinned_organizations
organization_pins.map(&:organization)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would load all pins of the user and map them in memory with N+1 queries.

Suggested change
organization_pins.map(&:organization)
object.pinned_organizations

We would loose the sorting though. I think we can probably remove the whole pinned_organizations field and only expose organization_pins.

Copy link
Copy Markdown
Member

@Taucher2003 Taucher2003 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the Namespaces::Members::DeleteService should remove an organization pin when a user is removed from that organization.

Comment thread app/models/user.rb
has_many :namespaces, through: :namespace_memberships, inverse_of: :users

has_many :user_identities, inverse_of: :user
has_many :user_organization_pins, -> { order(priority: :asc) }, inverse_of: :user, dependent: :delete_all
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use ON DELETE CASCADE in the schema, so we don't need this. Let the database handle the deletion of these records rather than rails doing it manually

Suggested change
has_many :user_organization_pins, -> { order(priority: :asc) }, inverse_of: :user, dependent: :delete_all
has_many :user_organization_pins, -> { order(priority: :asc) }, inverse_of: :user

class Organization < ApplicationRecord
include NamespaceParent

has_many :user_organization_pins, inverse_of: :organization, dependent: :delete_all
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use ON DELETE CASCADE in the schema, so we don't need this. Let the database handle the deletion of these records rather than rails doing it manually

Suggested change
has_many :user_organization_pins, inverse_of: :organization, dependent: :delete_all
has_many :user_organization_pins, inverse_of: :organization

Comment on lines +7 to +9
validates :priority, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :organization_id, uniqueness: { scope: :user_id }
validates :priority, uniqueness: { scope: :user_id }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works, but we can use a single validates for priority

Suggested change
validates :priority, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :organization_id, uniqueness: { scope: :user_id }
validates :priority, uniqueness: { scope: :user_id }
validates :priority, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 },
uniqueness: { scope: :user_id }
validates :organization_id, uniqueness: { scope: :user_id }

class UserOrganizationPinPolicy < BasePolicy
delegate { subject.user }

rule { can?(:read_user) }.enable :read_user_organization_pin
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either we don't need this here or we don't need the enable for this ability in the UserPolicy.

Also, I don't think someone should be able to read the pins of another user.

Suggested change
rule { can?(:read_user) }.enable :read_user_organization_pin
condition(:user_is_self) { subject.id == user&.id }
rule { user_is_self }.enable :read_user_organization_pin

Comment on lines +8 to +11
rule { ~anonymous }.policy do
enable :read_user
enable :read_user_organization_pin
end
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
rule { ~anonymous }.policy do
enable :read_user
enable :read_user_organization_pin
end
rule { ~anonymous }.enable :read_user

return ServiceResponse.error(message: 'Missing permission', error_code: :missing_permission)
end

organizations = Organization.where(id: organization_ids)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A user should only be able to pin organizations that it is a member of

Suggested change
organizations = Organization.where(id: organization_ids)
organizations = OrganizationsFinder.new(id: organization_ids, namespace_member_user: user).execute

end

AuditService.audit(
:user_updated,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be its own event


t.rollback_and_return! ServiceResponse.error(
message: 'Failed to update user organization pins',
error_code: :invalid_user,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably also be its own error code


pins = user.reload.user_organization_pins.order(:priority)
expect(pins.pluck(:organization_id)).to eq(organization_ids)
expect(pins.pluck(:priority)).to eq([0, 1])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will always be successful because the test orders the pins by priority

end

def execute
unless Ability.allowed?(current_authentication, :update_user, user)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using update_user allows an admin to change the pins of a user. If we don't want that, we need a separate ability

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow to pin organizations per user

2 participants