Merge lp:~jcsackett/launchpad/assertion-error-697294 into lp:launchpad

Proposed by j.c.sackett
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 12334
Proposed branch: lp:~jcsackett/launchpad/assertion-error-697294
Merge into: lp:launchpad
Diff against target: 258 lines (+68/-53)
4 files modified
lib/lp/answers/configure.zcml (+0/-6)
lib/lp/answers/model/question.py (+30/-5)
lib/lp/answers/model/questionreopening.py (+18/-32)
lib/lp/answers/tests/emailinterface.txt (+20/-10)
To merge this branch: bzr merge lp:~jcsackett/launchpad/assertion-error-697294
Reviewer Review Type Date Requested Status
Henning Eggers (community) code Approve
Review via email: mp+47911@code.launchpad.net

Commit message

[r=henninge][ui=none][bug=697294] Refactors create_questionreopening to be called as a helper, rather than a listener to notifications; this avoids the need for a slew of guards on the method, and keeps it from doing work when we don't want it to.

Description of the change

Summary
=======

In bug 697294, an assertion error would popup from a question in a bad state hitting question reopen (see bug 709816 about the bad state error). While the bad state is of low importance, question reopen was erroneously being called and attempting to do work it shouldn't do.

To handle this, create_questionreopening is refactored into a helper method, rather than notification listener that is called on all status events.

Preimplementation
=================

Spoke with Curtis Hovey.

Proposed Fix
============

Refactor the reopening method into a helper rather than a notification listener.

Implementation
==============

lib/lp/answers/configure.zcml
-----------------------------

Removed create_questionreopening from the list of notification subscribers.

lib/lp/answers/model/question.py
--------------------------------

Altered reopen and setStatus to call create_questionreopening directly.

lib/lp/answers/model/questionreopening.py
-----------------------------------------

create_questionreopening had its various guards removed, as it will only be called when a reopening will actually be called.

Tests
=====

bin/test -t answers.*workflow

Demo & QA
=========

Run through a workflow on answers with any question you can reopen and declare solved. All transitions should work as expected.

Lint
====

make lint output:

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/answers/configure.zcml
  lib/lp/answers/model/question.py
  lib/lp/answers/model/questionreopening.py

To post a comment you must log in.
Revision history for this message
Henning Eggers (henninge) wrote :

Thanks for doing this! All I really miss is a test that triggers the assertion error (without the fix), all in the spirit of TDD. ;-) Can you please add that test or explain why it is not needed?

Also, I am not sure if formatting one parameter per line always increases readability or just length.

Cheers,
Henning

review: Needs Fixing
Revision history for this message
j.c.sackett (jcsackett) wrote :

Henninge--

There's not actually a way to simulate the events that caused the assertion error--it requires a mid-air collision between email processing and web ui components that can't really be simulated in our test infrastructure. See bug 709816 for details on the situation that would need to be simulated.

This refactor codes around the circumstances to add some robustness so the bad state doesn't enter the code the assertion error occured in (by removing that block of code outright).

Revision history for this message
Henning Eggers (henninge) wrote :

OIC

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/answers/configure.zcml'
2--- lib/lp/answers/configure.zcml 2010-10-03 15:30:06 +0000
3+++ lib/lp/answers/configure.zcml 2011-02-04 16:55:00 +0000
4@@ -60,12 +60,6 @@
5 handler=".karma.question_comment_added"
6 />
7
8- <subscriber
9- for=".interfaces.question.IQuestion
10- lazr.lifecycle.interfaces.IObjectModifiedEvent"
11- handler=".model.questionreopening.create_questionreopening"
12- />
13-
14 <class class=".model.questionreopening.QuestionReopening">
15 <allow
16 interface=".interfaces.questionreopening.IQuestionReopening"
17
18=== modified file 'lib/lp/answers/model/question.py'
19--- lib/lp/answers/model/question.py 2010-11-15 21:56:43 +0000
20+++ lib/lp/answers/model/question.py 2011-02-04 16:55:00 +0000
21@@ -87,6 +87,7 @@
22 from lp.answers.interfaces.questiontarget import IQuestionTarget
23 from lp.answers.model.answercontact import AnswerContact
24 from lp.answers.model.questionmessage import QuestionMessage
25+from lp.answers.model.questionreopening import create_questionreopening
26 from lp.answers.model.questionsubscription import QuestionSubscription
27 from lp.app.enums import ServiceUsage
28 from lp.bugs.interfaces.buglink import IBugLinkTarget
29@@ -278,16 +279,29 @@
30 raise InvalidQuestionStateError(
31 "New status is same as the old one.")
32
33+
34 # If the previous state recorded an answer, clear those
35- # information as well.
36+ # information as well, but copy it out for the reopening.
37+ old_status = self.status
38+ old_answerer = self.answerer
39+ old_date_solved = self.date_solved
40 self.answerer = None
41 self.answer = None
42 self.date_solved = None
43
44- return self._newMessage(
45+ msg = self._newMessage(
46 user, comment, datecreated=datecreated,
47 action=QuestionAction.SETSTATUS, new_status=new_status)
48
49+ if new_status == QuestionStatus.OPEN:
50+ create_questionreopening(
51+ self,
52+ msg,
53+ old_status,
54+ old_answerer,
55+ old_date_solved)
56+ return msg
57+
58 @notify_question_modified()
59 def addComment(self, user, comment, datecreated=None):
60 """See `IQuestion`."""
61@@ -474,12 +488,24 @@
62 @notify_question_modified()
63 def reopen(self, comment, datecreated=None):
64 """See `IQuestion`."""
65+ old_status = self.status
66+ old_answerer = self.answerer
67+ old_date_solved = self.date_solved
68 if not self.can_reopen:
69 raise InvalidQuestionStateError(
70 "Question status != ANSWERED, EXPIRED or SOLVED.")
71 msg = self._newMessage(
72- self.owner, comment, datecreated=datecreated,
73- action=QuestionAction.REOPEN, new_status=QuestionStatus.OPEN)
74+ self.owner,
75+ comment,
76+ datecreated=datecreated,
77+ action=QuestionAction.REOPEN,
78+ new_status=QuestionStatus.OPEN)
79+ create_questionreopening(
80+ self,
81+ msg,
82+ old_status,
83+ old_answerer,
84+ old_date_solved)
85 self.answer = None
86 self.answerer = None
87 self.date_solved = None
88@@ -705,7 +731,6 @@
89 raise AssertionError(
90 'product_id and distribution_id are NULL')
91 return projects
92-
93
94 @staticmethod
95 def new(title=None, description=None, owner=None,
96
97=== modified file 'lib/lp/answers/model/questionreopening.py'
98--- lib/lp/answers/model/questionreopening.py 2010-10-03 15:30:06 +0000
99+++ lib/lp/answers/model/questionreopening.py 2011-02-04 16:55:00 +0000
100@@ -45,41 +45,27 @@
101 priorstate = EnumCol(schema=QuestionStatus, notNull=True)
102
103
104-def create_questionreopening(question, event):
105- """Event subscriber that creates a QuestionReopening event.
106+def create_questionreopening(
107+ question,
108+ reopen_msg,
109+ old_status,
110+ old_answerer,
111+ old_date_solved):
112+ """Helper function to handle question reopening.
113
114- A QuestionReopening is created question with an answer changes back to the
115- OPEN state.
116+ A QuestionReopening is created when question with an answer changes back
117+ to the OPEN state.
118 """
119- # XXX flacoste 2006-10-25 The QuestionReopening is probably not that
120- # useful anymore since the question history is nearly complete.
121- # If we decide to still keep that class, this subscriber should
122- # probably be moved outside of database code.
123- if question.status != QuestionStatus.OPEN:
124- return
125-
126- # Only create a QuestionReopening if the question had previsouly an
127- # answer.
128- old_question = event.object_before_modification
129- if old_question.answerer is None:
130- return
131- assert question.answerer is None, (
132- "Open question shouldn't have an answerer.")
133-
134- # The last added message is the cause of the reopening.
135- reopen_msg = question.messages[-1]
136-
137- # Make sure that the last message is really the last added one.
138- assert [reopen_msg] == (
139- list(set(question.messages).difference(old_question.messages))), (
140- "Reopening message isn't the last one.")
141-
142+ # XXX jcsackett This guard has to be maintained because reopen can
143+ # be called with the question in a bad state.
144+ if old_answerer is None:
145+ return
146 reopening = QuestionReopening(
147- question=question, reopener=reopen_msg.owner,
148+ question=question,
149+ reopener=reopen_msg.owner,
150 datecreated=reopen_msg.datecreated,
151- answerer=old_question.answerer,
152- date_solved=old_question.date_solved,
153- priorstate=old_question.status)
154-
155+ answerer=old_answerer,
156+ date_solved=old_date_solved,
157+ priorstate=old_status)
158 reopening = ProxyFactory(reopening)
159 notify(ObjectCreatedEvent(reopening, user=reopen_msg.owner))
160
161=== modified file 'lib/lp/answers/tests/emailinterface.txt'
162--- lib/lp/answers/tests/emailinterface.txt 2010-12-13 15:25:03 +0000
163+++ lib/lp/answers/tests/emailinterface.txt 2011-02-04 16:55:00 +0000
164@@ -1,4 +1,5 @@
165-= Answer Tracker Email Interface =
166+Answer Tracker Email Interface
167+==============================
168
169 The Answer Tracker has an email interface, although it's quite limited
170 at the moment. The only thing you can do is post new messages on the
171@@ -79,7 +80,8 @@
172 >>> comment_msgid is None
173 True
174
175-== Incoming Email and Workflow ==
176+Incoming Email and Workflow
177+---------------------------
178
179 With the way the Answer Tracker workflow is modelled (see
180 answer-tracker-workflow.txt for the details), adding a message will
181@@ -131,13 +133,15 @@
182 ... datecreated=now.next())
183 ... login('no-priv@canonical.com')
184
185-== Message From the Question Owner ==
186+Message From the Question Owner
187+-------------------------------
188
189 When the owner sends a message on the question, the message is
190 interpretated in three different manners based on the current question
191 state.
192
193-=== Open and Needs Information ===
194+Open and Needs Information
195+..........................
196
197 In the Open and Needs Information states, we assume the message provides
198 more information on the problem.
199@@ -180,7 +184,8 @@
200 correct our bad decision, since the question will stay on his
201 list of open questions.
202
203-=== Answered and Expired ===
204+Answered and Expired
205+....................
206
207 When the question is in the Answered or Expired states, we assume that
208 the email is reopening the question with more information.
209@@ -223,7 +228,8 @@
210 confusion, the outoing notification contain a footer explaining what
211 will happen if one reply to the message.
212
213-=== Solved and Invalid ===
214+Solved and Invalid
215+..................
216
217 When the question is in the Solved or Invalid state, we interpret the
218 message as a comment.
219@@ -262,7 +268,8 @@
220 state, there is no normal transition. The only possibility is that an
221 admin comes to change the status of the question.
222
223-=== Message From Another User ===
224+Message From Another User
225+.........................
226
227 It is simpler when a user other than the owner sends an email. When
228 the question is in the Open or Needs information state, there are only
229@@ -315,7 +322,8 @@
230 Answer
231
232
233-=== Solved, Invalid and Expired ===
234+Solved, Invalid and Expired
235+...........................
236
237 When another user than the owner sends a message to a question
238 in the Solved, Invalid or Expired states, the only possible
239@@ -357,7 +365,8 @@
240 Comment
241
242
243-=== Answers linked to FAQ questions ===
244+Answers linked to FAQ questions
245+...............................
246
247 Answers may also be linked to FAQ questions.
248
249@@ -387,7 +396,8 @@
250 Answer
251
252
253-== AnswerTrackerHandler Integration ==
254+AnswerTrackerHandler Integration
255+--------------------------------
256
257 The general mail processor delegates all emails to the
258 config.answertracker.email_domain to the AnswerTrackerHandler.