From 014e54d08c98c7f29b23da63a356ed7aa73081ee Mon Sep 17 00:00:00 2001 From: Dennis Meister Date: Sun, 12 Oct 2025 12:56:30 +0200 Subject: [PATCH 1/5] Add unique users for achievements --- .../types/achievements/achievement_type.rb | 5 +++ app/models/achievements/achievement.rb | 4 ++ .../achievements/achievement_type_spec.rb | 1 + spec/models/achievements/achievement_spec.rb | 38 +++++++++++++++++++ 4 files changed, 48 insertions(+) diff --git a/app/graphql/types/achievements/achievement_type.rb b/app/graphql/types/achievements/achievement_type.rb index 99f50966cbe45e..798d4f362d2331 100644 --- a/app/graphql/types/achievements/achievement_type.rb +++ b/app/graphql/types/achievements/achievement_type.rb @@ -51,6 +51,11 @@ class AchievementType < BaseObject extras: [:lookahead], resolver: ::Resolvers::Achievements::UserAchievementsResolver + field :unique_users, Types::UserType.connection_type, + null: false, + experiment: { milestone: '18.5' }, + description: "Count of unique users who have received the achievement." + def avatar_url object.avatar_url(only_path: false) end diff --git a/app/models/achievements/achievement.rb b/app/models/achievements/achievement.rb index d47868c4b669d9..6092132a77c63f 100644 --- a/app/models/achievements/achievement.rb +++ b/app/models/achievements/achievement.rb @@ -18,6 +18,10 @@ class Achievement < ApplicationRecord uniqueness: { case_sensitive: false, scope: [:namespace_id] } validates :description, length: { maximum: 1024 } + def unique_users + users.uniq + end + def uploads_sharding_key { namespace_id: namespace_id } end diff --git a/spec/graphql/types/achievements/achievement_type_spec.rb b/spec/graphql/types/achievements/achievement_type_spec.rb index 08fadcdff229fc..5e0eb1f69754a3 100644 --- a/spec/graphql/types/achievements/achievement_type_spec.rb +++ b/spec/graphql/types/achievements/achievement_type_spec.rb @@ -15,6 +15,7 @@ created_at updated_at user_achievements + unique_users ] end diff --git a/spec/models/achievements/achievement_spec.rb b/spec/models/achievements/achievement_spec.rb index 5d8d0e90bb0d58..3fdce249b65790 100644 --- a/spec/models/achievements/achievement_spec.rb +++ b/spec/models/achievements/achievement_spec.rb @@ -41,4 +41,42 @@ expect(achievement.uploads_sharding_key).to eq(namespace_id: namespace.id) end end + + describe '#unique_users' do + it 'returns unique users even when a user has multiple awards' do + achievement = create(:achievement) + user1 = create(:user) + user2 = create(:user) + user3 = create(:user) + + create(:user_achievement, achievement: achievement, user: user1) + create(:user_achievement, achievement: achievement, user: user1) + + create(:user_achievement, achievement: achievement, user: user2) + + unique_users = achievement.unique_users + + expect(unique_users.count).to eq(2) + expect(unique_users).to match_array([user1, user2]) + expect(unique_users).not_to include(user3) + end + + it 'returns empty when no users have been awarded' do + achievement = create(:achievement) + + expect(achievement.unique_users).to be_empty + end + + it 'returns single user when only one user has been awarded multiple times' do + achievement = create(:achievement) + user = create(:user) + + create(:user_achievement, achievement: achievement, user: user) + create(:user_achievement, achievement: achievement, user: user) + create(:user_achievement, achievement: achievement, user: user) + + expect(achievement.unique_users.count).to eq(1) + expect(achievement.unique_users).to eq([user]) + end + end end -- GitLab From 7f44e7027ce57783bef3ef6db1834269535e972a Mon Sep 17 00:00:00 2001 From: Dennis Meister Date: Mon, 13 Oct 2025 10:42:08 +0200 Subject: [PATCH 2/5] Fix review comments and pipeline --- app/graphql/types/achievements/achievement_type.rb | 2 +- app/models/achievements/achievement.rb | 2 +- doc/api/graphql/reference/_index.md | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/graphql/types/achievements/achievement_type.rb b/app/graphql/types/achievements/achievement_type.rb index 798d4f362d2331..2e1a1ff4511884 100644 --- a/app/graphql/types/achievements/achievement_type.rb +++ b/app/graphql/types/achievements/achievement_type.rb @@ -54,7 +54,7 @@ class AchievementType < BaseObject field :unique_users, Types::UserType.connection_type, null: false, experiment: { milestone: '18.5' }, - description: "Count of unique users who have received the achievement." + description: "Unique users who have received the achievement." def avatar_url object.avatar_url(only_path: false) diff --git a/app/models/achievements/achievement.rb b/app/models/achievements/achievement.rb index 6092132a77c63f..97bd13293678cf 100644 --- a/app/models/achievements/achievement.rb +++ b/app/models/achievements/achievement.rb @@ -19,7 +19,7 @@ class Achievement < ApplicationRecord validates :description, length: { maximum: 1024 } def unique_users - users.uniq + users.distinct end def uploads_sharding_key diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 013fd60e8381cb..054d8066a9b210 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -23143,6 +23143,7 @@ Representation of a GitLab user. | `id` | [`AchievementsAchievementID!`](#achievementsachievementid) | ID of the achievement. | | `name` | [`String!`](#string) | Name of the achievement. | | `namespace` | [`Namespace`](#namespace) | Namespace of the achievement. | +| `uniqueUsers` {{< icon name="warning-solid" >}} | [`UserCoreConnection!`](#usercoreconnection) | **Introduced** in GitLab 18.5. **Status**: Experiment. Unique users who have received the achievement. | | `updatedAt` | [`Time!`](#time) | Timestamp the achievement was last updated. | | `userAchievements` {{< icon name="warning-solid" >}} | [`UserAchievementConnection`](#userachievementconnection) | **Introduced** in GitLab 15.10. **Status**: Experiment. Recipients for the achievement. | -- GitLab From 2414c66d7b0e9742e20db9b32a7a73dd56fa2c06 Mon Sep 17 00:00:00 2001 From: Dennis Meister Date: Wed, 15 Oct 2025 09:03:06 +0200 Subject: [PATCH 3/5] Add uniqueUsers to n+1 spec --- .../graphql/achievements/user_achievements_query_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb b/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb index eb6e53442f3621..ca0bccddcf67a6 100644 --- a/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb +++ b/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb @@ -20,6 +20,12 @@ achievements { count nodes { + uniqueUsers { + count + nodes { + username + } + } userAchievements { count nodes { -- GitLab From 0ed90ba0e245a33da9d3bd03bb04314214a8dc41 Mon Sep 17 00:00:00 2001 From: Dennis Meister Date: Wed, 15 Oct 2025 16:43:26 +0200 Subject: [PATCH 4/5] Apply 3 suggestion(s) to 3 file(s) Co-authored-by: Lee Tickett --- .../types/achievements/achievement_type.rb | 2 +- doc/api/graphql/reference/_index.md | 2 +- spec/models/achievements/achievement_spec.rb | 28 ++++--------------- 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/app/graphql/types/achievements/achievement_type.rb b/app/graphql/types/achievements/achievement_type.rb index 2e1a1ff4511884..e6b27129f72ba2 100644 --- a/app/graphql/types/achievements/achievement_type.rb +++ b/app/graphql/types/achievements/achievement_type.rb @@ -53,7 +53,7 @@ class AchievementType < BaseObject field :unique_users, Types::UserType.connection_type, null: false, - experiment: { milestone: '18.5' }, + experiment: { milestone: '18.6' }, description: "Unique users who have received the achievement." def avatar_url diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 054d8066a9b210..3bb5e851e6dc67 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -23143,7 +23143,7 @@ Representation of a GitLab user. | `id` | [`AchievementsAchievementID!`](#achievementsachievementid) | ID of the achievement. | | `name` | [`String!`](#string) | Name of the achievement. | | `namespace` | [`Namespace`](#namespace) | Namespace of the achievement. | -| `uniqueUsers` {{< icon name="warning-solid" >}} | [`UserCoreConnection!`](#usercoreconnection) | **Introduced** in GitLab 18.5. **Status**: Experiment. Unique users who have received the achievement. | +| `uniqueUsers` {{< icon name="warning-solid" >}} | [`UserCoreConnection!`](#usercoreconnection) | **Introduced** in GitLab 18.6. **Status**: Experiment. Unique users who have received the achievement. | | `updatedAt` | [`Time!`](#time) | Timestamp the achievement was last updated. | | `userAchievements` {{< icon name="warning-solid" >}} | [`UserAchievementConnection`](#userachievementconnection) | **Introduced** in GitLab 15.10. **Status**: Experiment. Recipients for the achievement. | diff --git a/spec/models/achievements/achievement_spec.rb b/spec/models/achievements/achievement_spec.rb index 3fdce249b65790..eb228284bfb62e 100644 --- a/spec/models/achievements/achievement_spec.rb +++ b/spec/models/achievements/achievement_spec.rb @@ -43,40 +43,24 @@ end describe '#unique_users' do + let_it_be(:achievement) { create(:achievement) } + + subject(:unique_users) { achievement.unique_users } + it 'returns unique users even when a user has multiple awards' do - achievement = create(:achievement) user1 = create(:user) user2 = create(:user) user3 = create(:user) create(:user_achievement, achievement: achievement, user: user1) create(:user_achievement, achievement: achievement, user: user1) - create(:user_achievement, achievement: achievement, user: user2) - unique_users = achievement.unique_users - - expect(unique_users.count).to eq(2) - expect(unique_users).to match_array([user1, user2]) - expect(unique_users).not_to include(user3) + expect(unique_users).to contain_exactly(user1, user2) end it 'returns empty when no users have been awarded' do - achievement = create(:achievement) - - expect(achievement.unique_users).to be_empty - end - - it 'returns single user when only one user has been awarded multiple times' do - achievement = create(:achievement) - user = create(:user) - - create(:user_achievement, achievement: achievement, user: user) - create(:user_achievement, achievement: achievement, user: user) - create(:user_achievement, achievement: achievement, user: user) - - expect(achievement.unique_users.count).to eq(1) - expect(achievement.unique_users).to eq([user]) + expect(unique_users).to be_empty end end end -- GitLab From 5808a04fc6857f1d25c29c621c3df19d8a529f62 Mon Sep 17 00:00:00 2001 From: Dennis Meister Date: Wed, 15 Oct 2025 16:52:08 +0200 Subject: [PATCH 5/5] Remove unneeded user3 from spec --- spec/models/achievements/achievement_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/models/achievements/achievement_spec.rb b/spec/models/achievements/achievement_spec.rb index eb228284bfb62e..bd25b6d21ad966 100644 --- a/spec/models/achievements/achievement_spec.rb +++ b/spec/models/achievements/achievement_spec.rb @@ -50,7 +50,6 @@ it 'returns unique users even when a user has multiple awards' do user1 = create(:user) user2 = create(:user) - user3 = create(:user) create(:user_achievement, achievement: achievement, user: user1) create(:user_achievement, achievement: achievement, user: user1) -- GitLab