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