Skip to content

Commit f3344c6

Browse files
committed
Make the "read_tickets" permission not required to see and interact with the attendees list
1 parent 04dcda9 commit f3344c6

File tree

11 files changed

+98
-48
lines changed

11 files changed

+98
-48
lines changed

app/graphql/graphql_operations_generated.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

app/graphql/types/ability_type.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ class Types::AbilityType < Types::BaseObject
7171

7272
field :can_read_reports, Boolean, null: false
7373

74+
field :can_read_tickets, Boolean, null: false
75+
7476
field :can_manage_forms, Boolean, null: false
7577

7678
field :can_manage_oauth_applications, Boolean, null: false
@@ -255,6 +257,10 @@ def can_read_reports
255257
!!(convention && convention_policy.view_reports?)
256258
end
257259

260+
def can_read_tickets
261+
!!(convention && policy(Ticket.new(user_con_profile: UserConProfile.new(convention:))).read?)
262+
end
263+
258264
def can_manage_forms
259265
!!(convention && policy(Form.new(convention:)).manage?)
260266
end

app/graphql/types/user_con_profile_type.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ def site_admin
127127
:signups,
128128
:signup_ranked_choices,
129129
:signup_requests,
130-
:ticket,
131130
:user
132131

133132
def bio_html
@@ -206,6 +205,11 @@ def can_override_maximum_event_provided_tickets
206205
Pundit.policy(user, override).create?
207206
end
208207

208+
def ticket
209+
ticket = dataloader.with(Sources::ActiveRecordAssociation, UserConProfile, :ticket).load(object)
210+
ticket && policy(ticket).read? ? ticket : nil
211+
end
212+
209213
private
210214

211215
# Not exposed as a field, but needed by FormResponseAttrsFields

app/javascript/UserConProfiles/AttendeesPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ function AttendeesPage() {
2121
<UserConProfilesTable
2222
defaultVisibleColumns={['name', 'email', 'ticket', 'privileges']}
2323
attendeesPageQueryData={data}
24+
canReadTickets={data.currentAbility.can_read_tickets}
2425
/>
2526
<Outlet />
2627
</>

app/javascript/UserConProfiles/UserConProfilesTable.tsx

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,13 @@ function PrivilegesFilter<TData extends UserConProfilesTableRow, TValue>({
133133
export type UserConProfilesTableProps = {
134134
defaultVisibleColumns?: string[];
135135
attendeesPageQueryData: AttendeesPageQueryData;
136+
canReadTickets: boolean;
136137
};
137138

138139
function UserConProfilesTable({
139140
defaultVisibleColumns,
140141
attendeesPageQueryData,
142+
canReadTickets,
141143
}: UserConProfilesTableProps): JSX.Element {
142144
const { timezoneName } = useContext(AppRootContext);
143145
const { t } = useTranslation();
@@ -186,7 +188,7 @@ function UserConProfilesTable({
186188
}),
187189
];
188190

189-
if (attendeesPageQueryData.convention.ticket_mode !== TicketMode.Disabled) {
191+
if (attendeesPageQueryData.convention.ticket_mode !== TicketMode.Disabled && canReadTickets) {
190192
columns.push(
191193
columnHelper.accessor('ticket', {
192194
header: humanize(attendeesPageQueryData.convention.ticket_name || 'ticket'),
@@ -225,16 +227,21 @@ function UserConProfilesTable({
225227
enableColumnFilter: true,
226228
cell: BooleanCell,
227229
}),
228-
columnHelper.accessor((userConProfile) => userConProfile.ticket != null, {
229-
header: t('admin.userConProfiles.isAttending'),
230-
id: 'attending',
231-
size: 150,
232-
enableColumnFilter: true,
233-
cell: BooleanCell,
234-
}),
235230
);
236231

237-
if (attendeesPageQueryData.convention.ticket_mode !== TicketMode.Disabled) {
232+
if (canReadTickets) {
233+
columns.push(
234+
columnHelper.accessor((userConProfile) => userConProfile.ticket != null, {
235+
header: t('admin.userConProfiles.isAttending'),
236+
id: 'attending',
237+
size: 150,
238+
enableColumnFilter: true,
239+
cell: BooleanCell,
240+
}),
241+
);
242+
}
243+
244+
if (attendeesPageQueryData.convention.ticket_mode !== TicketMode.Disabled && canReadTickets) {
238245
columns.push(
239246
columnHelper.accessor(
240247
(userConProfile) =>
@@ -296,7 +303,7 @@ function UserConProfilesTable({
296303
});
297304

298305
return columns;
299-
}, [t, timezoneName, attendeesPageQueryData]);
306+
}, [t, timezoneName, attendeesPageQueryData, canReadTickets]);
300307

301308
const {
302309
table: tableInstance,

app/javascript/UserConProfiles/queries.generated.ts

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

app/javascript/UserConProfiles/queries.graphql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ query UserConProfileAdminQuery($id: ID!) {
134134
}
135135

136136
query AttendeesPageQuery {
137+
currentAbility {
138+
can_read_tickets
139+
}
140+
137141
convention: conventionByRequestHost {
138142
id
139143
ticket_name

app/javascript/graphqlTypes.generated.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export type Ability = {
6565
can_read_schedule: Scalars['Boolean']['output'];
6666
can_read_schedule_with_counts: Scalars['Boolean']['output'];
6767
can_read_signups: Scalars['Boolean']['output'];
68+
can_read_tickets: Scalars['Boolean']['output'];
6869
can_read_user_activity_alerts: Scalars['Boolean']['output'];
6970
can_read_user_con_profiles: Scalars['Boolean']['output'];
7071
can_read_users: Scalars['Boolean']['output'];

app/presenters/tables/user_con_profiles_table_results_presenter.rb

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
# frozen_string_literal: true
2-
require 'money-rails/helpers/action_view_extension'
2+
require "money-rails/helpers/action_view_extension"
33

44
class Tables::UserConProfilesTableResultsPresenter < Tables::TableResultsPresenter
55
extend MoneyRails::ActionViewExtension
66

7-
attr_reader :convention
7+
attr_reader :convention, :pundit_user
88

9-
def self.for_convention(convention, pundit_user, *args)
10-
new(convention, Pundit.policy_scope(pundit_user, convention.user_con_profiles), *args)
9+
def self.for_convention(convention, pundit_user, *)
10+
new(convention, pundit_user, Pundit.policy_scope(pundit_user, convention.user_con_profiles), *)
1111
end
1212

1313
def self.filter_ticket_type(scope, value)
14-
ticket_type_ids = value.map { |id_value| id_value == 'none' ? nil : id_value.to_i }
14+
ticket_type_ids = value.map { |id_value| id_value == "none" ? nil : id_value.to_i }
1515

1616
scope.left_joins(:ticket).where(tickets: { ticket_type_id: ticket_type_ids })
1717
end
1818

1919
def self.describe_ticket(ticket, include_payment_amount: true)
20-
return 'Unpaid' unless ticket
20+
return "Unpaid" unless ticket
2121

2222
status_parts = []
2323
status_parts << ticket.ticket_type.name.humanize
@@ -27,13 +27,13 @@ def self.describe_ticket(ticket, include_payment_amount: true)
2727
status_parts << humanized_money_with_symbol(payment_amount) if payment_amount.try(:>, 0)
2828
end
2929

30-
status_parts.compact.join(' ')
30+
status_parts.compact.join(" ")
3131
end
3232

33-
field :id, 'ID'
34-
field :user_id, 'User ID'
33+
field :id, "ID"
34+
field :user_id, "User ID"
3535

36-
field :name, 'Name' do
36+
field :name, "Name" do
3737
def apply_filter(scope, value)
3838
scope.name_search(value)
3939
end
@@ -50,7 +50,7 @@ def generate_csv_cell(user_con_profile)
5050
end
5151
end
5252

53-
field :first_name, 'First name' do
53+
field :first_name, "First name" do
5454
def apply_filter(scope, value)
5555
scope.name_search(value, columns: %w[first_name])
5656
end
@@ -60,7 +60,7 @@ def sql_order(direction)
6060
end
6161
end
6262

63-
field :last_name, 'Last name' do
63+
field :last_name, "Last name" do
6464
def apply_filter(scope, value)
6565
scope.name_search(value, columns: %w[last_name])
6666
end
@@ -70,9 +70,9 @@ def sql_order(direction)
7070
end
7171
end
7272

73-
field :email, 'Email' do
73+
field :email, "Email" do
7474
def apply_filter(scope, value)
75-
scope.joins(:user).where('lower(users.email) like :value', value: "%#{value.downcase}%")
75+
scope.joins(:user).where("lower(users.email) like :value", value: "%#{value.downcase}%")
7676
end
7777

7878
def expand_scope_for_sort(scope, _direction)
@@ -90,6 +90,7 @@ def csv_header
9090
end
9191

9292
def apply_filter(scope, value)
93+
return scope unless presenter.can_read_tickets?
9394
Tables::UserConProfilesTableResultsPresenter.filter_ticket_type(scope, value)
9495
end
9596

@@ -98,6 +99,7 @@ def expand_scope_for_sort(scope, _direction)
9899
end
99100

100101
def sql_order(direction)
102+
return unless presenter.can_read_tickets?
101103
Arel.sql("ticket_types.name #{direction}, order_entries.price_per_item_cents #{direction}")
102104
end
103105

@@ -112,6 +114,7 @@ def csv_header
112114
end
113115

114116
def apply_filter(scope, value)
117+
return scope unless presenter.can_read_tickets?
115118
Tables::UserConProfilesTableResultsPresenter.filter_ticket_type(scope, value)
116119
end
117120

@@ -120,6 +123,7 @@ def expand_scope_for_sort(scope, _direction)
120123
end
121124

122125
def sql_order(direction)
126+
return unless presenter.can_read_tickets?
123127
Arel.sql("ticket_types.name #{direction}")
124128
end
125129

@@ -131,13 +135,14 @@ def generate_csv_cell(user_con_profile)
131135
end
132136
end
133137

134-
field :payment_amount, 'Payment amount' do
138+
field :payment_amount, "Payment amount" do
135139
def apply_filter(scope, value)
140+
return scope unless presenter.can_read_tickets?
136141
payment_amount_fractional = (value.to_f * 100.0).to_i
137142
if payment_amount_fractional.zero?
138-
scope
139-
.left_joins(ticket: :order_entry)
140-
.where('order_entries.price_per_item_cents = 0 OR order_entries.price_per_item_cents IS NULL')
143+
scope.left_joins(ticket: :order_entry).where(
144+
"order_entries.price_per_item_cents = 0 OR order_entries.price_per_item_cents IS NULL"
145+
)
141146
else
142147
scope.left_joins(ticket: :order_entry).where(order_entries: { price_per_item_cents: payment_amount_fractional })
143148
end
@@ -149,7 +154,7 @@ def generate_csv_cell(user_con_profile)
149154
end
150155
end
151156

152-
field :is_team_member, 'Event team member?' do
157+
field :is_team_member, "Event team member?" do
153158
def apply_filter(scope, value)
154159
if value
155160
scope.where(id: TeamMember.for_active_events.select(:user_con_profile_id))
@@ -159,21 +164,18 @@ def apply_filter(scope, value)
159164
end
160165

161166
def generate_csv_cell(user_con_profile)
162-
user_con_profile.team_members.for_active_events.any? ? 'yes' : 'no'
167+
user_con_profile.team_members.for_active_events.any? ? "yes" : "no"
163168
end
164169
end
165170

166-
field :attending, 'Attending?' do
171+
field :attending, "Attending?" do
167172
def apply_filter(scope, value)
168-
if value
169-
scope.left_joins(:ticket).where.not(tickets: { id: nil })
170-
else
171-
scope.left_joins(:ticket).where(tickets: { id: nil })
172-
end
173+
return scope unless presenter.can_read_tickets?
174+
value ? scope.left_joins(:ticket).where.not(tickets: { id: nil }) : scope.where.missing(:ticket)
173175
end
174176

175177
def generate_csv_cell(user_con_profile)
176-
user_con_profile.ticket ? 'yes' : 'no'
178+
user_con_profile.ticket ? "yes" : "no"
177179
end
178180
end
179181

@@ -183,10 +185,12 @@ def csv_header
183185
end
184186

185187
def expand_scope_for_sort(scope, _direction)
188+
return scope unless presenter.can_read_tickets?
186189
scope.joins(ticket: :ticket_type)
187190
end
188191

189192
def sql_order(direction)
193+
return unless presenter.can_read_tickets?
190194
Arel.sql("tickets.updated_at #{direction}")
191195
end
192196

@@ -195,21 +199,27 @@ def generate_csv_cell(user_con_profile)
195199
end
196200
end
197201

198-
field :privileges, 'Privileges' do
202+
field :privileges, "Privileges" do
199203
def apply_filter(scope, value)
200-
value.include?('site_admin') ? scope.joins(:user).where(users: { site_admin: true }) : scope
204+
value.include?("site_admin") ? scope.joins(:user).where(users: { site_admin: true }) : scope
201205
end
202206

203207
def generate_csv_cell(user_con_profile)
204-
user_con_profile.privileges.map(&:titleize).sort.join(', ')
208+
user_con_profile.privileges.map(&:titleize).sort.join(", ")
205209
end
206210
end
207211

208-
field :order_summary, 'Order summary'
212+
field :order_summary, "Order summary"
209213

210-
def initialize(convention, *args)
214+
def initialize(convention, pundit_user, *)
211215
@convention = convention
212-
super(*args)
216+
@pundit_user = pundit_user
217+
super(*)
218+
end
219+
220+
def can_read_tickets?
221+
return @can_read_tickets if defined?(@can_read_tickets)
222+
@can_read_tickets = Pundit.policy(pundit_user, Ticket.new(user_con_profile: UserConProfile.new(convention:))).read?
213223
end
214224

215225
def fields
@@ -229,7 +239,7 @@ def form_fields
229239
private
230240

231241
def apply_privileges_filter(scope, value)
232-
value.include?('site_admin') ? scope.joins(:user).where(users: { site_admin: true }) : scope
242+
value.include?("site_admin") ? scope.joins(:user).where(users: { site_admin: true }) : scope
233243
end
234244

235245
def csv_scope

schema.graphql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type Ability {
3838
can_read_schedule: Boolean!
3939
can_read_schedule_with_counts: Boolean!
4040
can_read_signups: Boolean!
41+
can_read_tickets: Boolean!
4142
can_read_user_activity_alerts: Boolean!
4243
can_read_user_con_profiles: Boolean!
4344
can_read_users: Boolean!

0 commit comments

Comments
 (0)