blob: 3307cfde0e0fd00a64ea89049715e763bc59d4a1 [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2016 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Copybara854996b2021-09-07 19:36:02 +00004
5"""A set of functions that provide persistence for users.
6
7Business objects are described in user_pb2.py.
8"""
9from __future__ import print_function
10from __future__ import division
11from __future__ import absolute_import
12
13import logging
14import time
15
16import settings
17from framework import exceptions
18from framework import framework_bizobj
19from framework import framework_constants
20from framework import framework_helpers
21from framework import sql
22from framework import validate
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010023from mrproto import user_pb2
Copybara854996b2021-09-07 19:36:02 +000024from services import caches
25
26
27USER_TABLE_NAME = 'User'
28USERPREFS_TABLE_NAME = 'UserPrefs'
29HOTLISTVISITHISTORY_TABLE_NAME = 'HotlistVisitHistory'
30LINKEDACCOUNT_TABLE_NAME = 'LinkedAccount'
31LINKEDACCOUNTINVITE_TABLE_NAME = 'LinkedAccountInvite'
32
33USER_COLS = [
34 'user_id', 'email', 'is_site_admin', 'notify_issue_change',
35 'notify_starred_issue_change', 'email_compact_subject', 'email_view_widget',
36 'notify_starred_ping',
37 'banned', 'after_issue_update', 'keep_people_perms_open',
38 'preview_on_hover', 'obscure_email',
39 'last_visit_timestamp', 'email_bounce_timestamp', 'vacation_message']
40USERPREFS_COLS = ['user_id', 'name', 'value']
41HOTLISTVISITHISTORY_COLS = ['hotlist_id', 'user_id', 'viewed']
42LINKEDACCOUNT_COLS = ['parent_id', 'child_id']
43LINKEDACCOUNTINVITE_COLS = ['parent_id', 'child_id']
44
45
46class UserTwoLevelCache(caches.AbstractTwoLevelCache):
47 """Class to manage RAM and memcache for User PBs."""
48
49 def __init__(self, cache_manager, user_service):
50 super(UserTwoLevelCache, self).__init__(
51 cache_manager, 'user', 'user:', user_pb2.User,
52 max_size=settings.user_cache_max_size)
53 self.user_service = user_service
54
55 def _DeserializeUsersByID(self, user_rows, linkedaccount_rows):
56 """Convert database row tuples into User PBs.
57
58 Args:
59 user_rows: rows from the User DB table.
60 linkedaccount_rows: rows from the LinkedAccount DB table.
61
62 Returns:
63 A dict {user_id: user_pb} for all the users referenced in user_rows.
64 """
65 result_dict = {}
66
67 # Make one User PB for each row in user_rows.
68 for row in user_rows:
69 (user_id, email, is_site_admin,
70 notify_issue_change, notify_starred_issue_change,
71 email_compact_subject, email_view_widget, notify_starred_ping, banned,
72 after_issue_update, keep_people_perms_open, preview_on_hover,
73 obscure_email, last_visit_timestamp,
74 email_bounce_timestamp, vacation_message) = row
75 user = user_pb2.MakeUser(
76 user_id, email=email, obscure_email=obscure_email)
77 user.is_site_admin = bool(is_site_admin)
78 user.notify_issue_change = bool(notify_issue_change)
79 user.notify_starred_issue_change = bool(notify_starred_issue_change)
80 user.email_compact_subject = bool(email_compact_subject)
81 user.email_view_widget = bool(email_view_widget)
82 user.notify_starred_ping = bool(notify_starred_ping)
83 if banned:
84 user.banned = banned
85 if after_issue_update:
86 user.after_issue_update = user_pb2.IssueUpdateNav(
87 after_issue_update.upper())
88 user.keep_people_perms_open = bool(keep_people_perms_open)
89 user.preview_on_hover = bool(preview_on_hover)
90 user.last_visit_timestamp = last_visit_timestamp or 0
91 user.email_bounce_timestamp = email_bounce_timestamp or 0
92 if vacation_message:
93 user.vacation_message = vacation_message
94 result_dict[user_id] = user
95
96 # Put in any linked accounts.
97 for parent_id, child_id in linkedaccount_rows:
98 if parent_id in result_dict:
99 result_dict[parent_id].linked_child_ids.append(child_id)
100 if child_id in result_dict:
101 result_dict[child_id].linked_parent_id = parent_id
102
103 return result_dict
104
105 def FetchItems(self, cnxn, keys):
106 """On RAM and memcache miss, retrieve User objects from the database.
107
108 Args:
109 cnxn: connection to SQL database.
110 keys: list of user IDs to retrieve.
111
112 Returns:
113 A dict {user_id: user_pb} for each user that satisfies the conditions.
114 """
115 user_rows = self.user_service.user_tbl.Select(
116 cnxn, cols=USER_COLS, user_id=keys)
117 linkedaccount_rows = self.user_service.linkedaccount_tbl.Select(
118 cnxn, cols=LINKEDACCOUNT_COLS, parent_id=keys, child_id=keys,
119 or_where_conds=True)
120 return self._DeserializeUsersByID(user_rows, linkedaccount_rows)
121
122
123class UserPrefsTwoLevelCache(caches.AbstractTwoLevelCache):
124 """Class to manage RAM and memcache for UserPrefs PBs."""
125
126 def __init__(self, cache_manager, user_service):
127 super(UserPrefsTwoLevelCache, self).__init__(
128 cache_manager, 'user', 'userprefs:', user_pb2.UserPrefs,
129 max_size=settings.user_cache_max_size)
130 self.user_service = user_service
131
132 def _DeserializeUserPrefsByID(self, userprefs_rows):
133 """Convert database row tuples into UserPrefs PBs.
134
135 Args:
136 userprefs_rows: rows from the UserPrefs DB table.
137
138 Returns:
139 A dict {user_id: userprefs} for all the users in userprefs_rows.
140 """
141 result_dict = {}
142
143 # Make one UserPrefs PB for each row in userprefs_rows.
144 for row in userprefs_rows:
145 (user_id, name, value) = row
146 if user_id not in result_dict:
147 userprefs = user_pb2.UserPrefs(user_id=user_id)
148 result_dict[user_id] = userprefs
149 else:
150 userprefs = result_dict[user_id]
151 userprefs.prefs.append(user_pb2.UserPrefValue(name=name, value=value))
152
153 return result_dict
154
155 def FetchItems(self, cnxn, keys):
156 """On RAM and memcache miss, retrieve UserPrefs objects from the database.
157
158 Args:
159 cnxn: connection to SQL database.
160 keys: list of user IDs to retrieve.
161
162 Returns:
163 A dict {user_id: userprefs} for each user.
164 """
165 userprefs_rows = self.user_service.userprefs_tbl.Select(
166 cnxn, cols=USERPREFS_COLS, user_id=keys)
167 return self._DeserializeUserPrefsByID(userprefs_rows)
168
169
170class UserService(object):
171 """The persistence layer for all user data."""
172
173 def __init__(self, cache_manager):
174 """Constructor.
175
176 Args:
177 cache_manager: local cache with distributed invalidation.
178 """
179 self.user_tbl = sql.SQLTableManager(USER_TABLE_NAME)
180 self.userprefs_tbl = sql.SQLTableManager(USERPREFS_TABLE_NAME)
181 self.hotlistvisithistory_tbl = sql.SQLTableManager(
182 HOTLISTVISITHISTORY_TABLE_NAME)
183 self.linkedaccount_tbl = sql.SQLTableManager(LINKEDACCOUNT_TABLE_NAME)
184 self.linkedaccountinvite_tbl = sql.SQLTableManager(
185 LINKEDACCOUNTINVITE_TABLE_NAME)
186
187 # Like a dictionary {user_id: email}
188 self.email_cache = caches.RamCache(cache_manager, 'user', max_size=50000)
189
190 # Like a dictionary {email: user_id}.
191 # This will never invaidate, and it doesn't need to.
192 self.user_id_cache = caches.RamCache(cache_manager, 'user', max_size=50000)
193
194 # Like a dictionary {user_id: user_pb}
195 self.user_2lc = UserTwoLevelCache(cache_manager, self)
196
197 # Like a dictionary {user_id: userprefs}
198 self.userprefs_2lc = UserPrefsTwoLevelCache(cache_manager, self)
199
200 ### Creating users
201
202 def _CreateUsers(self, cnxn, emails):
203 """Create many users in the database."""
204 emails = [email.lower() for email in emails]
205 ids = [framework_helpers.MurmurHash3_x86_32(email) for email in emails]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100206
207 rows = self.user_tbl.Select(cnxn, cols=('user_id',), user_id=ids)
208 existing_ids = set(row[0] for row in rows)
209 if existing_ids:
210 existing_users = sorted(
211 (user_id, email)
212 for (user_id, email) in zip(ids, emails)
213 if user_id in existing_ids)
214 logging.error(
215 'Unable to create users because IDs are already taken: %.100000s',
216 existing_users)
217
Copybara854996b2021-09-07 19:36:02 +0000218 row_values = [
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100219 (user_id, email, not framework_bizobj.IsPriviledgedDomainUser(email))
220 for (user_id, email) in zip(ids, emails)
221 if user_id not in existing_ids
222 ]
Copybara854996b2021-09-07 19:36:02 +0000223 self.user_tbl.InsertRows(
224 cnxn, ['user_id', 'email', 'obscure_email'], row_values)
225 self.user_2lc.InvalidateKeys(cnxn, ids)
226
227 ### Lookup of user ID and email address
228
229 def LookupUserEmails(self, cnxn, user_ids, ignore_missed=False):
230 """Return a dict of email addresses for the given user IDs.
231
232 Args:
233 cnxn: connection to SQL database.
234 user_ids: list of int user IDs to look up.
235 ignore_missed: if True, does not throw NoSuchUserException, when there
236 are users not found for some user_ids.
237
238 Returns:
239 A dict {user_id: email_addr} for all the requested IDs.
240
241 Raises:
242 exceptions.NoSuchUserException: if any requested user cannot be found
243 and ignore_missed is False.
244 """
245 self.email_cache.CacheItem(framework_constants.NO_USER_SPECIFIED, '')
246 emails_dict, missed_ids = self.email_cache.GetAll(user_ids)
247 if missed_ids:
248 logging.info('got %d user emails from cache', len(emails_dict))
249 rows = self.user_tbl.Select(
250 cnxn, cols=['user_id', 'email'], user_id=missed_ids)
251 retrieved_dict = dict(rows)
252 logging.info('looked up users %r', retrieved_dict)
253 self.email_cache.CacheAll(retrieved_dict)
254 emails_dict.update(retrieved_dict)
255
256 # Check if there are any that we could not find. ID 0 means "no user".
257 nonexist_ids = [user_id for user_id in user_ids
258 if user_id and user_id not in emails_dict]
259 if nonexist_ids:
260 if ignore_missed:
261 logging.info('No email addresses found for users %r' % nonexist_ids)
262 else:
263 raise exceptions.NoSuchUserException(
264 'No email addresses found for users %r' % nonexist_ids)
265
266 return emails_dict
267
268 def LookupUserEmail(self, cnxn, user_id):
269 """Get the email address of the given user.
270
271 Args:
272 cnxn: connection to SQL database.
273 user_id: int user ID of the user whose email address is needed.
274
275 Returns:
276 String email address of that user or None if user_id is invalid.
277
278 Raises:
279 exceptions.NoSuchUserException: if no email address was found for that
280 user.
281 """
282 if not user_id:
283 return None
284 emails_dict = self.LookupUserEmails(cnxn, [user_id])
285 return emails_dict[user_id]
286
287 def LookupExistingUserIDs(self, cnxn, emails):
288 """Return a dict of user IDs for the given emails for users that exist.
289
290 Args:
291 cnxn: connection to SQL database.
292 emails: list of string email addresses.
293
294 Returns:
295 A dict {email_addr: user_id} for the requested emails.
296 """
297 # Look up these users in the RAM cache
298 user_id_dict, missed_emails = self.user_id_cache.GetAll(emails)
299
300 # Hit the DB to lookup any user IDs that were not cached.
301 if missed_emails:
302 rows = self.user_tbl.Select(
303 cnxn, cols=['email', 'user_id'], email=missed_emails)
304 retrieved_dict = dict(rows)
305 # Cache all the user IDs that we retrieved to make later requests faster.
306 self.user_id_cache.CacheAll(retrieved_dict)
307 user_id_dict.update(retrieved_dict)
308
309 return user_id_dict
310
311 def LookupUserIDs(self, cnxn, emails, autocreate=False,
312 allowgroups=False):
313 """Return a dict of user IDs for the given emails.
314
315 Args:
316 cnxn: connection to SQL database.
317 emails: list of string email addresses.
318 autocreate: set to True to create users that were not found.
319 allowgroups: set to True to allow non-email user name for group
320 creation.
321
322 Returns:
323 A dict {email_addr: user_id} for the requested emails.
324
325 Raises:
326 exceptions.NoSuchUserException: if some users were not found and
327 autocreate is False.
328 """
329 # Skip any addresses that look like "--" or are empty,
330 # because that means "no user".
331 # Also, make sure all email addresses are lower case.
332 needed_emails = [email.lower() for email in emails
333 if email
334 and not framework_constants.NO_VALUE_RE.match(email)]
335
336 # Look up these users in the RAM cache
337 user_id_dict = self.LookupExistingUserIDs(cnxn, needed_emails)
338 if len(needed_emails) == len(user_id_dict):
339 return user_id_dict
340
341 # If any were not found in the DB, create them or raise an exception.
342 nonexist_emails = [email for email in needed_emails
343 if email not in user_id_dict]
344 logging.info('nonexist_emails: %r, autocreate is %r',
345 nonexist_emails, autocreate)
346 if not autocreate:
347 raise exceptions.NoSuchUserException('%r' % nonexist_emails)
348
349 if not allowgroups:
350 # Only create accounts for valid email addresses.
351 nonexist_emails = [email for email in nonexist_emails
352 if validate.IsValidEmail(email)]
353 if not nonexist_emails:
354 return user_id_dict
355
356 self._CreateUsers(cnxn, nonexist_emails)
357 created_rows = self.user_tbl.Select(
358 cnxn, cols=['email', 'user_id'], email=nonexist_emails)
359 created_dict = dict(created_rows)
360 # Cache all the user IDs that we retrieved to make later requests faster.
361 self.user_id_cache.CacheAll(created_dict)
362 user_id_dict.update(created_dict)
363
364 logging.info('looked up User IDs %r', user_id_dict)
365 return user_id_dict
366
367 def LookupUserID(self, cnxn, email, autocreate=False, allowgroups=False):
368 """Get one user ID for the given email address.
369
370 Args:
371 cnxn: connection to SQL database.
372 email: string email address of the user to look up.
373 autocreate: set to True to create users that were not found.
374 allowgroups: set to True to allow non-email user name for group
375 creation.
376
377 Returns:
378 The int user ID of the specified user.
379
380 Raises:
381 exceptions.NoSuchUserException if the user was not found and autocreate
382 is False.
383 """
384 email = email.lower()
385 email_dict = self.LookupUserIDs(
386 cnxn, [email], autocreate=autocreate, allowgroups=allowgroups)
387 if email not in email_dict:
388 raise exceptions.NoSuchUserException('%r not found' % email)
389 return email_dict[email]
390
391 ### Retrieval of user objects: with preferences and cues
392
393 def GetUsersByIDs(self, cnxn, user_ids, use_cache=True, skip_missed=False):
394 """Return a dictionary of retrieved User PBs.
395
396 Args:
397 cnxn: connection to SQL database.
398 user_ids: list of user IDs to fetch.
399 use_cache: set to False to ignore cache and force DB lookup.
400 skip_missed: set to True if default User objects for missed_ids should
401 not be created.
402
403 Returns:
404 A dict {user_id: user_pb} for each specified user ID. For any user ID
405 that is not fount in the DB, a default User PB is created on-the-fly.
406 """
407 # Check the RAM cache and memcache, as appropriate.
408 result_dict, missed_ids = self.user_2lc.GetAll(
409 cnxn, user_ids, use_cache=use_cache)
410
411 # TODO(crbug/monorail/7367): Never create default values for missed_ids
412 # once we remove all code paths that hit this. See bug for more info.
413 # Any new code that calls this method, should not rely on this
414 # functionality.
415 if missed_ids and not skip_missed:
416 # Provide default values for any user ID that was not found.
417 result_dict.update(
418 (user_id, user_pb2.MakeUser(user_id)) for user_id in missed_ids)
419
420 return result_dict
421
422 def GetUser(self, cnxn, user_id):
423 """Load the specified user from the user details table."""
424 return self.GetUsersByIDs(cnxn, [user_id])[user_id]
425
426 ### Updating user objects
427
428 def UpdateUser(self, cnxn, user_id, user):
429 """Store a user PB in the database.
430
431 Args:
432 cnxn: connection to SQL database.
433 user_id: int user ID of the user to update.
434 user: User PB to store.
435
436 Returns:
437 Nothing.
438 """
439 if not user_id:
440 raise exceptions.NoSuchUserException('Cannot update anonymous user')
441
442 delta = {
443 'is_site_admin': user.is_site_admin,
444 'notify_issue_change': user.notify_issue_change,
445 'notify_starred_issue_change': user.notify_starred_issue_change,
446 'email_compact_subject': user.email_compact_subject,
447 'email_view_widget': user.email_view_widget,
448 'notify_starred_ping': user.notify_starred_ping,
449 'banned': user.banned,
450 'after_issue_update': str(user.after_issue_update or 'UP_TO_LIST'),
451 'keep_people_perms_open': user.keep_people_perms_open,
452 'preview_on_hover': user.preview_on_hover,
453 'obscure_email': user.obscure_email,
454 'last_visit_timestamp': user.last_visit_timestamp,
455 'email_bounce_timestamp': user.email_bounce_timestamp,
456 'vacation_message': user.vacation_message,
457 }
458 # Start sending UPDATE statements, but don't COMMIT until the end.
459 self.user_tbl.Update(cnxn, delta, user_id=user_id, commit=False)
460
461 cnxn.Commit()
462 self.user_2lc.InvalidateKeys(cnxn, [user_id])
463
464 def UpdateUserBan(
465 self, cnxn, user_id, user,
466 is_banned=None, banned_reason=None):
467 if is_banned is not None:
468 if is_banned:
469 user.banned = banned_reason or 'No reason given'
470 else:
471 user.reset('banned')
472
473 # Write the user settings to the database.
474 self.UpdateUser(cnxn, user_id, user)
475
476 def GetRecentlyVisitedHotlists(self, cnxn, user_id):
477 recent_hotlist_rows = self.hotlistvisithistory_tbl.Select(
478 cnxn, cols=['hotlist_id'], user_id=[user_id],
479 order_by=[('viewed DESC', [])], limit=10)
480 return [row[0] for row in recent_hotlist_rows]
481
482 def AddVisitedHotlist(self, cnxn, user_id, hotlist_id, commit=True):
483 self.hotlistvisithistory_tbl.Delete(
484 cnxn, hotlist_id=hotlist_id, user_id=user_id, commit=False)
485 self.hotlistvisithistory_tbl.InsertRows(
486 cnxn, HOTLISTVISITHISTORY_COLS,
487 [(hotlist_id, user_id, int(time.time()))],
488 commit=commit)
489
490 def ExpungeHotlistsFromHistory(self, cnxn, hotlist_ids, commit=True):
491 self.hotlistvisithistory_tbl.Delete(
492 cnxn, hotlist_id=hotlist_ids, commit=commit)
493
494 def ExpungeUsersHotlistsHistory(self, cnxn, user_ids, commit=True):
495 self.hotlistvisithistory_tbl.Delete(cnxn, user_id=user_ids, commit=commit)
496
497 def TrimUserVisitedHotlists(self, cnxn, commit=True):
498 """For any user who has visited more than 10 hotlists, trim history."""
499 user_id_rows = self.hotlistvisithistory_tbl.Select(
500 cnxn, cols=['user_id'], group_by=['user_id'],
501 having=[('COUNT(*) > %s', [10])], limit=1000)
502
503 for user_id in [row[0] for row in user_id_rows]:
504 viewed_hotlist_rows = self.hotlistvisithistory_tbl.Select(
505 cnxn,
506 cols=['viewed'],
507 user_id=user_id,
508 order_by=[('viewed DESC', [])])
509 if len(viewed_hotlist_rows) > 10:
510 cut_off_date = viewed_hotlist_rows[9][0]
511 self.hotlistvisithistory_tbl.Delete(
512 cnxn,
513 user_id=user_id,
514 where=[('viewed < %s', [cut_off_date])],
515 commit=commit)
516
517 ### Linked account invites
518
519 def GetPendingLinkedInvites(self, cnxn, user_id):
520 """Return lists of accounts that have invited this account."""
521 if not user_id:
522 return [], []
523 invite_rows = self.linkedaccountinvite_tbl.Select(
524 cnxn, cols=LINKEDACCOUNTINVITE_COLS, parent_id=user_id,
525 child_id=user_id, or_where_conds=True)
526 invite_as_parent = [row[1] for row in invite_rows
527 if row[0] == user_id]
528 invite_as_child = [row[0] for row in invite_rows
529 if row[1] == user_id]
530 return invite_as_parent, invite_as_child
531
532 def _AssertNotAlreadyLinked(self, cnxn, parent_id, child_id):
533 """Check constraints on our linked account graph."""
534 # Our linked account graph should be no more than one level deep.
535 parent_is_already_a_child = self.linkedaccount_tbl.Select(
536 cnxn, cols=LINKEDACCOUNT_COLS, child_id=parent_id)
537 if parent_is_already_a_child:
538 raise exceptions.InputException('Parent account is already a child')
539 child_is_already_a_parent = self.linkedaccount_tbl.Select(
540 cnxn, cols=LINKEDACCOUNT_COLS, parent_id=child_id)
541 if child_is_already_a_parent:
542 raise exceptions.InputException('Child account is already a parent')
543
544 # A child account can only be linked to one parent.
545 child_is_already_a_child = self.linkedaccount_tbl.Select(
546 cnxn, cols=LINKEDACCOUNT_COLS, child_id=child_id)
547 if child_is_already_a_child:
548 raise exceptions.InputException('Child account is already linked')
549
550 def InviteLinkedParent(self, cnxn, parent_id, child_id):
551 """Child stores an invite for the proposed parent user to consider."""
552 if not parent_id:
553 raise exceptions.InputException('Parent account is missing')
554 if not child_id:
555 raise exceptions.InputException('Child account is missing')
556 self._AssertNotAlreadyLinked(cnxn, parent_id, child_id)
557 self.linkedaccountinvite_tbl.InsertRow(
558 cnxn, parent_id=parent_id, child_id=child_id)
559
560 def AcceptLinkedChild(self, cnxn, parent_id, child_id):
561 """Parent accepts an invite from a child account."""
562 if not parent_id:
563 raise exceptions.InputException('Parent account is missing')
564 if not child_id:
565 raise exceptions.InputException('Child account is missing')
566 # Check that the child has previously created an invite for this parent.
567 invite_rows = self.linkedaccountinvite_tbl.Select(
568 cnxn, cols=LINKEDACCOUNTINVITE_COLS,
569 parent_id=parent_id, child_id=child_id)
570 if not invite_rows:
571 raise exceptions.InputException('No such invite')
572
573 self._AssertNotAlreadyLinked(cnxn, parent_id, child_id)
574
575 self.linkedaccount_tbl.InsertRow(
576 cnxn, parent_id=parent_id, child_id=child_id)
577 self.linkedaccountinvite_tbl.Delete(
578 cnxn, parent_id=parent_id, child_id=child_id)
579 self.user_2lc.InvalidateKeys(cnxn, [parent_id, child_id])
580
581 def UnlinkAccounts(self, cnxn, parent_id, child_id):
582 """Delete a linked-account relationship."""
583 if not parent_id:
584 raise exceptions.InputException('Parent account is missing')
585 if not child_id:
586 raise exceptions.InputException('Child account is missing')
587 self.linkedaccount_tbl.Delete(
588 cnxn, parent_id=parent_id, child_id=child_id)
589 self.user_2lc.InvalidateKeys(cnxn, [parent_id, child_id])
590
591 ### User settings
592 # Settings are details about a user account that are usually needed
593 # every time that user is displayed to another user.
594
595 # TODO(jrobbins): Move most of these into UserPrefs.
596 def UpdateUserSettings(
597 self, cnxn, user_id, user, notify=None, notify_starred=None,
598 email_compact_subject=None, email_view_widget=None,
599 notify_starred_ping=None, obscure_email=None, after_issue_update=None,
600 is_site_admin=None, is_banned=None, banned_reason=None,
601 keep_people_perms_open=None, preview_on_hover=None,
602 vacation_message=None):
603 """Update the preferences of the specified user.
604
605 Args:
606 cnxn: connection to SQL database.
607 user_id: int user ID of the user whose settings we are updating.
608 user: User PB of user before changes are applied.
609 keyword args: dictionary of setting names mapped to new values.
610
611 Returns:
612 The user's new User PB.
613 """
614 # notifications
615 if notify is not None:
616 user.notify_issue_change = notify
617 if notify_starred is not None:
618 user.notify_starred_issue_change = notify_starred
619 if notify_starred_ping is not None:
620 user.notify_starred_ping = notify_starred_ping
621 if email_compact_subject is not None:
622 user.email_compact_subject = email_compact_subject
623 if email_view_widget is not None:
624 user.email_view_widget = email_view_widget
625
626 # display options
627 if after_issue_update is not None:
628 user.after_issue_update = user_pb2.IssueUpdateNav(after_issue_update)
629 if preview_on_hover is not None:
630 user.preview_on_hover = preview_on_hover
631 if keep_people_perms_open is not None:
632 user.keep_people_perms_open = keep_people_perms_open
633
634 # misc
635 if obscure_email is not None:
636 user.obscure_email = obscure_email
637
638 # admin
639 if is_site_admin is not None:
640 user.is_site_admin = is_site_admin
641 if is_banned is not None:
642 if is_banned:
643 user.banned = banned_reason or 'No reason given'
644 else:
645 user.reset('banned')
646
647 # user availability
648 if vacation_message is not None:
649 user.vacation_message = vacation_message
650
651 # Write the user settings to the database.
652 self.UpdateUser(cnxn, user_id, user)
653
654 ### User preferences
655 # These are separate from settings in the User objects because they are
656 # only needed for the currently signed in user.
657
658 def GetUsersPrefs(self, cnxn, user_ids, use_cache=True):
659 """Return {user_id: userprefs} for the requested user IDs."""
660 prefs_dict, misses = self.userprefs_2lc.GetAll(
661 cnxn, user_ids, use_cache=use_cache)
662 # Make sure that every user is represented in the result.
663 for user_id in misses:
664 prefs_dict[user_id] = user_pb2.UserPrefs(user_id=user_id)
665 return prefs_dict
666
667 def GetUserPrefs(self, cnxn, user_id, use_cache=True):
668 """Return a UserPrefs PB for the requested user ID."""
669 prefs_dict = self.GetUsersPrefs(cnxn, [user_id], use_cache=use_cache)
670 return prefs_dict[user_id]
671
672 def GetUserPrefsByEmail(self, cnxn, email, use_cache=True):
673 """Return a UserPrefs PB for the requested email, or an empty UserPrefs."""
674 try:
675 user_id = self.LookupUserID(cnxn, email)
676 user_prefs = self.GetUserPrefs(cnxn, user_id, use_cache=use_cache)
677 except exceptions.NoSuchUserException:
678 user_prefs = user_pb2.UserPrefs()
679 return user_prefs
680
681 def SetUserPrefs(self, cnxn, user_id, pref_values):
682 """Store the given list of UserPrefValues."""
683 userprefs_rows = [(user_id, upv.name, upv.value) for upv in pref_values]
684 self.userprefs_tbl.InsertRows(
685 cnxn, USERPREFS_COLS, userprefs_rows, replace=True)
686 self.userprefs_2lc.InvalidateKeys(cnxn, [user_id])
687
688 ### Expunge all User Data from DB
689
690 def ExpungeUsers(self, cnxn, user_ids):
691 """Completely wipes user data from User DB tables for given users.
692
693 This method will not commit the operation. This method will not make
694 changes to in-memory data.
695 NOTE: This method ends with an operation that deletes user rows. If
696 appropriate methods that remove references to the User table rows are
697 not called before, the commit will fail. See work_env.ExpungeUsers
698 for more info.
699
700 Args:
701 cnxn: connection to SQL database.
702 user_ids: list of user_ids for users we want to delete.
703 """
704 self.linkedaccount_tbl.Delete(cnxn, parent_id=user_ids, commit=False)
705 self.linkedaccount_tbl.Delete(cnxn, child_id=user_ids, commit=False)
706 self.linkedaccountinvite_tbl.Delete(cnxn, parent_id=user_ids, commit=False)
707 self.linkedaccountinvite_tbl.Delete(cnxn, child_id=user_ids, commit=False)
708 self.userprefs_tbl.Delete(cnxn, user_id=user_ids, commit=False)
709 self.user_tbl.Delete(cnxn, user_id=user_ids, commit=False)
710
711 def TotalUsersCount(self, cnxn):
712 """Returns the total number of rows in the User table.
713
714 The placeholder User reserved for representing deleted users within Monorail
715 will not be counted.
716 """
717 # Subtract one so we don't count the deleted user with
718 # with user_id = framework_constants.DELETED_USER_ID
719 return (self.user_tbl.SelectValue(cnxn, col='COUNT(*)')) - 1
720
721 def GetAllUserEmailsBatch(self, cnxn, limit=1000, offset=0):
722 """Returns a list of user emails.
723
724 This method can be used for listing all user emails in Monorail's DB.
725 The list will contain at most [limit] emails, and be ordered by
726 user_id. The list will start at the given offset value. The email for
727 the placeholder User reserved for representing deleted users within
728 Monorail will never be returned.
729
730 Args:
731 cnxn: connection to SQL database.
732 limit: limit on the number of emails returned, defaults to 1000.
733 offset: starting index of the list, defaults to 0.
734
735 """
736 rows = self.user_tbl.Select(
737 cnxn, cols=['email'],
738 limit=limit,
739 offset=offset,
740 where=[('user_id != %s', [framework_constants.DELETED_USER_ID])],
741 order_by=[('user_id ASC', [])])
742 return [row[0] for row in rows]