Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(490)

Side by Side Diff: appengine/monorail/features/filterrules_helpers.py

Issue 1868553004: Open Source Monorail (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Rebase Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2016 The Chromium Authors. All rights reserved.
2 # Use of this source code is govered 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 """Implementation of the filter rules helper functions."""
7
8 import logging
9 import re
10
11 from google.appengine.api import taskqueue
12
13 import settings
14 from framework import framework_bizobj
15 from framework import framework_constants
16 from framework import monorailrequest
17 from framework import urls
18 from framework import validate
19 from proto import ast_pb2
20 from proto import tracker_pb2
21 from search import query2ast
22 from search import searchpipeline
23 from services import user_svc
24 from tracker import component_helpers
25 from tracker import tracker_bizobj
26 from tracker import tracker_constants
27 from tracker import tracker_helpers
28
29
30 # Maximum number of filer rules that can be specified in a given
31 # project. This helps us bound the amount of time needed to
32 # (re)compute derived fields.
33 MAX_RULES = 200
34
35 BLOCK = tracker_constants.RECOMPUTE_DERIVED_FIELDS_BLOCK_SIZE
36
37
38 # TODO(jrobbins): implement a more efficient way to update just those
39 # issues affected by a specific component change.
40 def RecomputeAllDerivedFields(cnxn, services, project, config):
41 """Create work items to update all issues after filter rule changes.
42
43 Args:
44 cnxn: connection to SQL database.
45 services: connections to backend services.
46 project: Project PB for the project that was edited.
47 config: ProjectIssueConfig PB for the project that was edited,
48 including the edits made.
49 """
50 if not settings.recompute_derived_fields_in_worker:
51 # Background tasks are not enabled, just do everything in the servlet.
52 RecomputeAllDerivedFieldsNow(cnxn, services, project, config)
53 return
54
55 highest_id = services.issue.GetHighestLocalID(cnxn, project.project_id)
56 if highest_id == 0:
57 return # No work to do.
58
59 # Enqueue work items for blocks of issues to recompute.
60 steps = range(1, highest_id + 1, BLOCK)
61 steps.reverse() # Update higher numbered issues sooner, old issues last.
62 # Cycle through shard_ids just to load-balance among the replicas. Each
63 # block includes all issues in that local_id range, not just 1/10 of them.
64 shard_id = 0
65 for step in steps:
66 params = {
67 'project_id': project.project_id,
68 'lower_bound': step,
69 'upper_bound': min(step + BLOCK, highest_id + 1),
70 'shard_id': shard_id,
71 }
72 logging.info('adding task with params %r', params)
73 taskqueue.add(
74 url=urls.RECOMPUTE_DERIVED_FIELDS_TASK + '.do', params=params)
75 shard_id = (shard_id + 1) % settings.num_logical_shards
76
77
78 def RecomputeAllDerivedFieldsNow(
79 cnxn, services, project, config, lower_bound=None, upper_bound=None,
80 shard_id=None):
81 """Re-apply all filter rules to all issues in a project.
82
83 Args:
84 cnxn: connection to SQL database.
85 services: connections to persistence layer.
86 project: Project PB for the project that was changed.
87 config: ProjectIssueConfig for that project.
88 lower_bound: optional int lowest issue ID to consider, inclusive.
89 upper_bound: optional int highest issue ID to consider, exclusive.
90 shard_id: optional int shard_id to read from one replica.
91
92 SIDE-EFFECT: updates all issues in the project. Stores and re-indexes
93 all those that were changed.
94 """
95 if lower_bound is not None and upper_bound is not None:
96 issues = services.issue.GetIssuesByLocalIDs(
97 cnxn, project.project_id, range(lower_bound, upper_bound),
98 shard_id=shard_id)
99 else:
100 issues = services.issue.GetAllIssuesInProject(cnxn, project.project_id)
101
102 rules = services.features.GetFilterRules(cnxn, project.project_id)
103 predicate_asts = ParsePredicateASTs(rules, config, None)
104 modified_issues = []
105 for issue in issues:
106 if ApplyGivenRules(cnxn, services, issue, config, rules, predicate_asts):
107 modified_issues.append(issue)
108
109 services.issue.UpdateIssues(cnxn, modified_issues, just_derived=True)
110
111 # Doing the FTS indexing can be too slow, so queue up the issues
112 # that need to be re-indexed by a cron-job later.
113 services.issue.EnqueueIssuesForIndexing(
114 cnxn, [issue.issue_id for issue in modified_issues])
115
116
117 def ParsePredicateASTs(rules, config, me_user_id):
118 """Parse the given rules in QueryAST PBs."""
119 predicates = [rule.predicate for rule in rules]
120 if me_user_id:
121 predicates = [searchpipeline.ReplaceKeywordsWithUserID(me_user_id, pred)
122 for pred in predicates]
123 predicate_asts = [
124 query2ast.ParseUserQuery(pred, '', query2ast.BUILTIN_ISSUE_FIELDS, config)
125 for pred in predicates]
126 return predicate_asts
127
128
129 def ApplyFilterRules(cnxn, services, issue, config):
130 """Apply the filter rules for this project to the given issue.
131
132 Args:
133 cnxn: database connection, used to look up user IDs.
134 services: persistence layer for users, issues, and projects.
135 issue: An Issue PB that has just been updated with new explicit values.
136 config: The project's issue tracker config PB.
137
138 Returns:
139 True if any derived_* field of the issue was changed.
140
141 SIDE-EFFECT: update the derived_* fields of the Issue PB.
142 """
143 rules = services.features.GetFilterRules(cnxn, issue.project_id)
144 predicate_asts = ParsePredicateASTs(rules, config, None)
145 return ApplyGivenRules(cnxn, services, issue, config, rules, predicate_asts)
146
147
148 def ApplyGivenRules(cnxn, services, issue, config, rules, predicate_asts):
149 """Apply the filter rules for this project to the given issue.
150
151 Args:
152 cnxn: database connection, used to look up user IDs.
153 services: persistence layer for users, issues, and projects.
154 issue: An Issue PB that has just been updated with new explicit values.
155 config: The project's issue tracker config PB.
156 rules: list of FilterRule PBs.
157
158 Returns:
159 True if any derived_* field of the issue was changed.
160
161 SIDE-EFFECT: update the derived_* fields of the Issue PB.
162 """
163 (derived_owner_id, derived_status, derived_cc_ids,
164 derived_labels, derived_notify_addrs) = _ComputeDerivedFields(
165 cnxn, services, issue, config, rules, predicate_asts)
166
167 any_change = (derived_owner_id != issue.derived_owner_id or
168 derived_status != issue.derived_status or
169 derived_cc_ids != issue.derived_cc_ids or
170 derived_labels != issue.derived_labels or
171 derived_notify_addrs != issue.derived_notify_addrs)
172
173 # Remember any derived values.
174 issue.derived_owner_id = derived_owner_id
175 issue.derived_status = derived_status
176 issue.derived_cc_ids = derived_cc_ids
177 issue.derived_labels = derived_labels
178 issue.derived_notify_addrs = derived_notify_addrs
179
180 return any_change
181
182
183 def _ComputeDerivedFields(cnxn, services, issue, config, rules, predicate_asts):
184 """Compute derived field values for an issue based on filter rules.
185
186 Args:
187 cnxn: database connection, used to look up user IDs.
188 services: persistence layer for users, issues, and projects.
189 issue: the issue to examine.
190 config: ProjectIssueConfig for the project containing the issue.
191 rules: list of FilterRule PBs.
192 predicate_asts: QueryAST PB for each rule.
193
194 Returns:
195 A 5-tuple of derived values for owner_id, status, cc_ids, labels, and
196 notify_addrs. These values are the result of applying all rules in order.
197 Filter rules only produce derived values that do not conflict with the
198 explicit field values of the issue.
199 """
200 excl_prefixes = config.exclusive_label_prefixes
201 # Examine the explicit labels and Cc's on the issue.
202 lower_labels = [lab.lower() for lab in issue.labels]
203 label_set = set(lower_labels)
204 cc_set = set(issue.cc_ids)
205 excl_prefixes_used = set()
206 for lab in lower_labels:
207 prefix = lab.split('-')[0]
208 if prefix in excl_prefixes:
209 excl_prefixes_used.add(prefix)
210 prefix_values_added = {}
211
212 # Start with the assumption that rules don't change anything, then
213 # accumulate changes.
214 derived_owner_id = framework_constants.NO_USER_SPECIFIED
215 derived_status = ''
216 # Get the component auto-cc's before even starting the rules.
217 # TODO(jrobbins): take this out and instead get component CC IDs
218 # on each access and search, but that will be a pretty big change.
219 derived_cc_ids = [
220 auto_cc_id
221 for auto_cc_id in component_helpers.GetComponentCcIDs(issue, config)
222 if auto_cc_id not in cc_set]
223 derived_labels = []
224 derived_notify_addrs = []
225
226 # Apply each rule in order. Later rules see the results of earlier rules.
227 # Later rules can overwrite or add to results of earlier rules.
228 # TODO(jrobbins): also pass in in-progress values for owner and CCs so
229 # that early rules that set those can affect later rules that check them.
230 for rule, predicate_ast in zip(rules, predicate_asts):
231 (rule_owner_id, rule_status, rule_add_cc_ids,
232 rule_add_labels, rule_add_notify) = _ApplyRule(
233 cnxn, services, rule, predicate_ast, issue, label_set, config)
234
235 # logging.info(
236 # 'rule "%s" gave %r, %r, %r, %r, %r',
237 # rule.predicate, rule_owner_id, rule_status, rule_add_cc_ids,
238 # rule_add_labels, rule_add_notify)
239
240 if rule_owner_id and not issue.owner_id:
241 derived_owner_id = rule_owner_id
242
243 if rule_status and not issue.status:
244 derived_status = rule_status
245
246 for cc_id in rule_add_cc_ids:
247 if cc_id not in cc_set:
248 derived_cc_ids.append(cc_id)
249 cc_set.add(cc_id)
250
251 for lab in rule_add_labels:
252 lab_lower = lab.lower()
253 if lab_lower in label_set:
254 continue # We already have that label.
255 prefix = lab_lower.split('-')[0]
256 if '-' in lab_lower and prefix in excl_prefixes:
257 if prefix in excl_prefixes_used:
258 continue # Issue already has that prefix.
259 # Replace any earlied-added label that had the same exclusive prefix.
260 if prefix in prefix_values_added:
261 label_set.remove(prefix_values_added[prefix].lower())
262 derived_labels = [dl for dl in derived_labels
263 if dl != prefix_values_added[prefix]]
264 prefix_values_added[prefix] = lab
265
266 derived_labels.append(lab)
267 label_set.add(lab_lower)
268
269 for addr in rule_add_notify:
270 if addr not in derived_notify_addrs:
271 derived_notify_addrs.append(addr)
272
273 return (derived_owner_id, derived_status, derived_cc_ids, derived_labels,
274 derived_notify_addrs)
275
276
277 def EvalPredicate(
278 cnxn, services, predicate_ast, issue, label_set, config, owner_id, cc_ids,
279 status):
280 """Return True if the given issue satisfies the given predicate.
281
282 Args:
283 cnxn: Connection to SQL database.
284 services: persistence layer for users and issues.
285 predicate_ast: QueryAST for rule or saved query string.
286 issue: Issue PB of the issue to evaluate.
287 label_set: set of lower-cased labels on the issue.
288 config: ProjectIssueConfig for the project that contains the issue.
289 owner_id: int user ID of the issue owner.
290 cc_ids: list of int user IDs of the users Cc'd on the issue.
291 status: string status value of the issue.
292
293 Returns:
294 True if the issue satisfies the predicate.
295
296 Note: filter rule evaluation passes in only the explicit owner_id,
297 cc_ids, and status whereas subscription evaluation passes in the
298 combination of explicit values and derived values.
299 """
300 # TODO(jrobbins): Call ast2ast to simplify the predicate and do
301 # most lookups. Refactor to allow that to be done once.
302 project = services.project.GetProject(cnxn, config.project_id)
303 for conj in predicate_ast.conjunctions:
304 if all(_ApplyCond(cnxn, services, project, cond, issue, label_set, config,
305 owner_id, cc_ids, status)
306 for cond in conj.conds):
307 return True
308
309 # All OR-clauses were evaluated, but none of them was matched.
310 return False
311
312
313 def _ApplyRule(
314 cnxn, services, rule_pb, predicate_ast, issue, label_set, config):
315 """Test if the given rule should fire and return its result.
316
317 Args:
318 cnxn: database connection, used to look up user IDs.
319 services: persistence layer for users and issues.
320 rule_pb: FilterRule PB instance with a predicate and various actions.
321 predicate_ast: QueryAST for the rule predicate.
322 issue: The Issue PB to be considered.
323 label_set: set of lowercased labels from an issue's explicit
324 label_list plus and labels that have accumlated from previous rules.
325 config: ProjectIssueConfig for the project containing the issue.
326
327 Returns:
328 A 5-tuple of the results from this rule: derived owner id, status,
329 cc_ids to add, labels to add, and notify addresses to add.
330 """
331 if EvalPredicate(
332 cnxn, services, predicate_ast, issue, label_set, config,
333 issue.owner_id, issue.cc_ids, issue.status):
334 logging.info('rule adds: %r', rule_pb.add_labels)
335 return (rule_pb.default_owner_id, rule_pb.default_status,
336 rule_pb.add_cc_ids, rule_pb.add_labels,
337 rule_pb.add_notify_addrs)
338 else:
339 return None, None, [], [], []
340
341
342 def _ApplyCond(
343 cnxn, services, project, term, issue, label_set, config, owner_id, cc_ids,
344 status):
345 """Return True if the given issue satisfied the given predicate term."""
346 op = term.op
347 vals = term.str_values or term.int_values
348 # Since rules are per-project, there'll be exactly 1 field
349 fd = term.field_defs[0]
350 field = fd.field_name
351
352 if field == 'label':
353 return _Compare(op, vals, label_set)
354 if field == 'component':
355 return _CompareComponents(config, op, vals, issue.component_ids)
356 if field == 'any_field':
357 return _Compare(op, vals, label_set) or _Compare(op, vals, [issue.summary])
358 if field == 'attachments':
359 return _Compare(op, vals, [issue.attachment_count])
360 if field == 'blocked':
361 return _Compare(op, vals, issue.blocked_on_iids)
362 if field == 'blockedon':
363 return _CompareIssueRefs(
364 cnxn, services, project, op, term.str_values, issue.blocked_on_iids)
365 if field == 'blocking':
366 return _CompareIssueRefs(
367 cnxn, services, project, op, term.str_values, issue.blocking_iids)
368 if field == 'cc':
369 return _CompareUsers(cnxn, services.user, op, vals, cc_ids)
370 if field == 'closed':
371 return (issue.closed_timestamp and
372 _Compare(op, vals, [issue.closed_timestamp]))
373 if field == 'id':
374 return _Compare(op, vals, [issue.local_id])
375 if field == 'mergedinto':
376 return _CompareIssueRefs(
377 cnxn, services, project, op, term.str_values, [issue.merged_into or 0])
378 if field == 'modified':
379 return (issue.modified_timestamp and
380 _Compare(op, vals, [issue.modified_timestamp]))
381 if field == 'open':
382 # TODO(jrobbins): this just checks the explicit status, not the result
383 # of any previous rules.
384 return tracker_helpers.MeansOpenInProject(status, config)
385 if field == 'opened':
386 return (issue.opened_timestamp and
387 _Compare(op, vals, [issue.opened_timestamp]))
388 if field == 'owner':
389 return _CompareUsers(cnxn, services.user, op, vals, [owner_id])
390 if field == 'reporter':
391 return _CompareUsers(cnxn, services.user, op, vals, [issue.reporter_id])
392 if field == 'stars':
393 return _Compare(op, vals, [issue.star_count])
394 if field == 'status':
395 return _Compare(op, vals, [status.lower()])
396 if field == 'summary':
397 return _Compare(op, vals, [issue.summary])
398
399 # Since rules are per-project, it makes no sense to support field project.
400 # We would need to load comments to support fields comment, commentby,
401 # description, attachment.
402 # Supporting starredby is probably not worth the complexity.
403
404 logging.info('Rule with unsupported field %r was False', field)
405 return False
406
407
408 def _CheckTrivialCases(op, issue_values):
409 """Check has:x and -has:x terms and no values. Otherwise, return None."""
410 # We can do these operators without looking up anything or even knowing
411 # which field is being checked.
412 issue_values_exist = bool(
413 issue_values and issue_values != [''] and issue_values != [0])
414 if op == ast_pb2.QueryOp.IS_DEFINED:
415 return issue_values_exist
416 elif op == ast_pb2.QueryOp.IS_NOT_DEFINED:
417 return not issue_values_exist
418 elif not issue_values_exist:
419 # No other operator can match empty values.
420 return op in (ast_pb2.QueryOp.NE, ast_pb2.QueryOp.NOT_TEXT_HAS)
421
422 return None # Caller should continue processing the term.
423
424 def _CompareComponents(config, op, rule_values, issue_values):
425 """Compare the components specified in the rule vs those in the issue."""
426 trivial_result = _CheckTrivialCases(op, issue_values)
427 if trivial_result is not None:
428 return trivial_result
429
430 exact = op in (ast_pb2.QueryOp.EQ, ast_pb2.QueryOp.NE)
431 rule_component_ids = set()
432 for path in rule_values:
433 rule_component_ids.update(tracker_bizobj.FindMatchingComponentIDs(
434 path, config, exact=exact))
435
436 if op == ast_pb2.QueryOp.TEXT_HAS or op == ast_pb2.QueryOp.EQ:
437 return any(rv in issue_values for rv in rule_component_ids)
438 elif op == ast_pb2.QueryOp.NOT_TEXT_HAS or op == ast_pb2.QueryOp.NE:
439 return all(rv not in issue_values for rv in rule_component_ids)
440
441 return False
442
443
444 def _CompareIssueRefs(
445 cnxn, services, project, op, rule_str_values, issue_values):
446 """Compare the issues specified in the rule vs referenced in the issue."""
447 trivial_result = _CheckTrivialCases(op, issue_values)
448 if trivial_result is not None:
449 return trivial_result
450
451 rule_refs = []
452 for str_val in rule_str_values:
453 ref = tracker_bizobj.ParseIssueRef(str_val)
454 if ref:
455 rule_refs.append(ref)
456 rule_ref_project_names = set(
457 pn for pn, local_id in rule_refs if pn)
458 rule_ref_projects_dict = services.project.GetProjectsByName(
459 cnxn, rule_ref_project_names)
460 rule_ref_projects_dict[project.project_name] = project
461 rule_iids = services.issue.ResolveIssueRefs(
462 cnxn, rule_ref_projects_dict, project.project_name, rule_refs)
463
464 if op == ast_pb2.QueryOp.TEXT_HAS:
465 op = ast_pb2.QueryOp.EQ
466 if op == ast_pb2.QueryOp.NOT_TEXT_HAS:
467 op = ast_pb2.QueryOp.NE
468
469 return _Compare(op, rule_iids, issue_values)
470
471
472 def _CompareUsers(cnxn, user_service, op, rule_values, issue_values):
473 """Compare the user(s) specified in the rule and the issue."""
474 # Note that all occurances of "me" in rule_values should have already
475 # been resolved to str(user_id) of the subscribing user.
476 # TODO(jrobbins): Project filter rules should not be allowed to have "me".
477
478 trivial_result = _CheckTrivialCases(op, issue_values)
479 if trivial_result is not None:
480 return trivial_result
481
482 try:
483 return _CompareUserIDs(op, rule_values, issue_values)
484 except ValueError:
485 return _CompareEmails(cnxn, user_service, op, rule_values, issue_values)
486
487
488 def _CompareUserIDs(op, rule_values, issue_values):
489 """Compare users according to specified user ID integer strings."""
490 rule_user_ids = [int(uid_str) for uid_str in rule_values]
491
492 if op == ast_pb2.QueryOp.TEXT_HAS or op == ast_pb2.QueryOp.EQ:
493 return any(rv in issue_values for rv in rule_user_ids)
494 elif op == ast_pb2.QueryOp.NOT_TEXT_HAS or op == ast_pb2.QueryOp.NE:
495 return all(rv not in issue_values for rv in rule_user_ids)
496
497 logging.info('unexpected numeric user operator %r %r %r',
498 op, rule_values, issue_values)
499 return False
500
501
502 def _CompareEmails(cnxn, user_service, op, rule_values, issue_values):
503 """Compare users based on email addresses."""
504 issue_emails = user_service.LookupUserEmails(cnxn, issue_values).values()
505
506 if op == ast_pb2.QueryOp.TEXT_HAS:
507 return any(_HasText(rv, issue_emails) for rv in rule_values)
508 elif op == ast_pb2.QueryOp.NOT_TEXT_HAS:
509 return all(not _HasText(rv, issue_emails) for rv in rule_values)
510 elif op == ast_pb2.QueryOp.EQ:
511 return any(rv in issue_emails for rv in rule_values)
512 elif op == ast_pb2.QueryOp.NE:
513 return all(rv not in issue_emails for rv in rule_values)
514
515 logging.info('unexpected user operator %r %r %r',
516 op, rule_values, issue_values)
517 return False
518
519
520 def _Compare(op, rule_values, issue_values):
521 """Compare the values specified in the rule and the issue."""
522 trivial_result = _CheckTrivialCases(op, issue_values)
523 if trivial_result is not None:
524 return trivial_result
525
526 if (op in [ast_pb2.QueryOp.TEXT_HAS, ast_pb2.QueryOp.NOT_TEXT_HAS] and
527 issue_values and not isinstance(min(issue_values), basestring)):
528 return False # Empty or numeric fields cannot match substrings
529 elif op == ast_pb2.QueryOp.TEXT_HAS:
530 return any(_HasText(rv, issue_values) for rv in rule_values)
531 elif op == ast_pb2.QueryOp.NOT_TEXT_HAS:
532 return all(not _HasText(rv, issue_values) for rv in rule_values)
533
534 val_type = type(min(issue_values))
535 if val_type == int or val_type == long:
536 try:
537 rule_values = [int(rv) for rv in rule_values]
538 except ValueError:
539 logging.info('rule value conversion to int failed: %r', rule_values)
540 return False
541
542 if op == ast_pb2.QueryOp.EQ:
543 return any(rv in issue_values for rv in rule_values)
544 elif op == ast_pb2.QueryOp.NE:
545 return all(rv not in issue_values for rv in rule_values)
546
547 if val_type != int and val_type != long:
548 return False # Inequalities only work on numeric fields
549
550 if op == ast_pb2.QueryOp.GT:
551 return min(issue_values) > min(rule_values)
552 elif op == ast_pb2.QueryOp.GE:
553 return min(issue_values) >= min(rule_values)
554 elif op == ast_pb2.QueryOp.LT:
555 return max(issue_values) < max(rule_values)
556 elif op == ast_pb2.QueryOp.LE:
557 return max(issue_values) <= max(rule_values)
558
559 logging.info('unexpected operator %r %r %r', op, rule_values, issue_values)
560 return False
561
562
563 def _HasText(rule_text, issue_values):
564 """Return True if the issue contains the rule text, case insensitive."""
565 rule_lower = rule_text.lower()
566 for iv in issue_values:
567 if iv is not None and rule_lower in iv.lower():
568 return True
569
570 return False
571
572
573 def MakeRule(predicate, default_status=None, default_owner_id=None,
574 add_cc_ids=None, add_labels=None, add_notify=None):
575 """Make a FilterRule PB with the supplied information.
576
577 Args:
578 predicate: string query that will trigger the rule if satisfied.
579 default_status: optional default status to set if rule fires.
580 default_owner_id: optional default owner_id to set if rule fires.
581 add_cc_ids: optional cc ids to set if rule fires.
582 add_labels: optional label strings to set if rule fires.
583 add_notify: optional notify email addresses to set if rule fires.
584
585 Returns:
586 A new FilterRule PB.
587 """
588 rule_pb = tracker_pb2.FilterRule()
589 rule_pb.predicate = predicate
590
591 if add_labels:
592 rule_pb.add_labels = add_labels
593 if default_status:
594 rule_pb.default_status = default_status
595 if default_owner_id:
596 rule_pb.default_owner_id = default_owner_id
597 if add_cc_ids:
598 rule_pb.add_cc_ids = add_cc_ids
599 if add_notify:
600 rule_pb.add_notify_addrs = add_notify
601
602 return rule_pb
603
604
605 def ParseRules(cnxn, post_data, user_service, errors, prefix=''):
606 """Parse rules from the user and return a list of FilterRule PBs.
607
608 Args:
609 cnxn: connection to database.
610 post_data: dictionary of html form data.
611 user_service: connection to user backend services.
612 errors: EZTErrors message used to display field validation errors.
613 prefix: optional string prefix used to differentiate the form fields
614 for existing rules from the form fields for new rules.
615
616 Returns:
617 A list of FilterRule PBs
618 """
619 rules = []
620
621 # The best we can do for now is show all validation errors at the bottom of
622 # the filter rules section, not directly on the rule that had the error :(.
623 error_list = []
624
625 for i in xrange(1, MAX_RULES + 1):
626 if ('%spredicate%s' % (prefix, i)) not in post_data:
627 continue # skip any entries that are blank or have no predicate.
628 predicate = post_data['%spredicate%s' % (prefix, i)].strip()
629 action_type = post_data.get('%saction_type%s' % (prefix, i),
630 'add_labels').strip()
631 action_value = post_data.get('%saction_value%s' % (prefix, i),
632 '').strip()
633 if predicate:
634 # Note: action_value may be '', meaning no-op.
635 rules.append(_ParseOneRule(
636 cnxn, predicate, action_type, action_value, user_service, i,
637 error_list))
638
639 if error_list:
640 errors.rules = error_list
641
642 return rules
643
644
645 def _ParseOneRule(
646 cnxn, predicate, action_type, action_value, user_service,
647 rule_num, error_list):
648 """Parse one FilterRule based on the action type."""
649 if action_type == 'default_status':
650 status = framework_bizobj.CanonicalizeLabel(action_value)
651 rule = MakeRule(predicate, default_status=status)
652
653 elif action_type == 'default_owner':
654 if action_value:
655 try:
656 user_id = user_service.LookupUserID(cnxn, action_value)
657 except user_svc.NoSuchUserException:
658 user_id = framework_constants.NO_USER_SPECIFIED
659 error_list.append(
660 'Rule %d: No such user: %s' % (rule_num, action_value))
661 else:
662 user_id = framework_constants.NO_USER_SPECIFIED
663 rule = MakeRule(predicate, default_owner_id=user_id)
664
665 elif action_type == 'add_ccs':
666 cc_ids = []
667 for email in re.split('[,;\s]+', action_value):
668 if not email.strip():
669 continue
670 try:
671 user_id = user_service.LookupUserID(
672 cnxn, email.strip(), autocreate=True)
673 cc_ids.append(user_id)
674 except user_svc.NoSuchUserException:
675 error_list.append(
676 'Rule %d: No such user: %s' % (rule_num, email.strip()))
677
678 rule = MakeRule(predicate, add_cc_ids=cc_ids)
679
680 elif action_type == 'add_labels':
681 add_labels = framework_constants.IDENTIFIER_RE.findall(action_value)
682 rule = MakeRule(predicate, add_labels=add_labels)
683
684 elif action_type == 'also_notify':
685 add_notify = []
686 for addr in re.split('[,;\s]+', action_value):
687 if validate.IsValidEmail(addr.strip()):
688 add_notify.append(addr.strip())
689 else:
690 error_list.append(
691 'Rule %d: Invalid email address: %s' % (rule_num, addr.strip()))
692
693 rule = MakeRule(predicate, add_notify=add_notify)
694
695 else:
696 logging.info('unexpected action type, probably tampering:%r', action_type)
697 raise monorailrequest.InputException()
698
699 return rule
OLDNEW
« no previous file with comments | « appengine/monorail/features/filterrules.py ('k') | appengine/monorail/features/filterrules_views.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698