~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
Questions Expiration
====================

It is not productive to have questions lying around forever in
the Answer Tracker. That's why we have a script which runs daily to
expire old questions on which there was no activity for the past two
weeks.

The expiration period is set using the
config.answertracker.days_before_expiration configuration variable. It
defaults to 15 days.

    >>> from canonical.config import config
    >>> config.answertracker.days_before_expiration
    15

Only questions in the OPEN or NEEDSINFO state which aren't assigned to
somebody are subject to expiration.

    # Sanity check in case somebody modifies the question sampledata and
    # forget to update this script.
    >>> from lp.answers.enums import QuestionStatus
    >>> from lp.answers.model.question import Question
    >>> Question.select('status IN (%i,%i)' % (
    ...     QuestionStatus.OPEN.value,
    ...     QuestionStatus.NEEDSINFO.value)).count()
    9

    # By default, all open and needs info question should expire. Make
    # sure that no new questions were recently added and will make this
    # test fails in the future.
    >>> Question.select(
    ...     "datelastresponse >= current_timestamp - interval '15 days' OR "
    ...    "datelastquery >= current_timestamp - interval '15 days'").count()
    0

    # We need to massage sample data a little. Since all expiration
    # candidates in sample data would expire, do a little activity on
    # some of these.
    >>> from datetime import datetime, timedelta
    >>> from pytz import UTC
    >>> now = datetime.now(UTC)
    >>> two_weeks_ago = now - timedelta(days=14)
    >>> a_month_ago = now - timedelta(days=31)
    >>> from canonical.launchpad.webapp.interfaces import ILaunchBag
    >>> from lp.answers.interfaces.questioncollection import IQuestionSet
    >>> from lp.registry.interfaces.person import IPersonSet
    >>> login('no-priv@canonical.com')
    >>> no_priv = getUtility(ILaunchBag).user

    >>> questionset = getUtility(IQuestionSet)

    # An old question in NEEDSINFO the state.
    >>> old_needs_info_question = questionset.get(7)
    >>> print old_needs_info_question.status.title
    Needs information

    # An open question assigned to somebody.
    >>> login('foo.bar@canonical.com')
    >>> old_assigned_open_question = questionset.get(1)
    >>> old_assigned_open_question.assignee = getUtility(ILaunchBag).user

    # This one got an update from its owner recently.
    >>> login('test@canonical.com')
    >>> recent_open_question = questionset.get(2)
    >>> recent_open_question.giveInfo(
    ...     'SVG works better now, but is still broken')
    <QuestionMessage...>

    # This one was put in the NEEDSINFO state recently.
    >>> recent_needsinfo_question = questionset.get(4)
    >>> recent_needsinfo_question.requestInfo(
    ...     no_priv, 'What URL were you visiting?')
    <QuestionMessage...>

    # Old open questions.
    >>> old_open_question = questionset.get(5)

    # Subscribe a team to that question, and a answer contact,
    # to make sure that DB permissions are correct.
    >>> admin_team = getUtility(IPersonSet).getByName('admins')
    >>> old_open_question.subscribe(admin_team)
    <QuestionSubscription...>
    >>> salgado = getUtility(IPersonSet).getByName('salgado')
    >>> old_open_question.target.addAnswerContact(salgado, salgado)
    True

    # Link it to a FAQ item for the same reason. We are setting the
    # attribute directly, because using the linkFAQ API would update
    # the last updates date of the question and remove it from the expiration
    # set.
    >>> from zope.security.proxy import removeSecurityProxy
    >>> login('foo.bar@canonical.com')
    >>> faq = old_open_question.target.newFAQ(
    ...     salgado, 'Why everyone think this is weird.',
    ...     "That's an easy one. It's because it is!")
    >>> removeSecurityProxy(old_open_question).faq = faq

    # A question linked to an non-Invalid bug is not expirable.
    >>> from lp.bugs.interfaces.bug import IBugSet
    >>> from lp.bugs.interfaces.bugtask import BugTaskStatus
    >>> fixed_bug = getUtility(IBugSet).get(9)
    >>> bugtasks = fixed_bug.bugtasks
    >>> bugtasks[1].transitionToStatus(BugTaskStatus.INVALID, no_priv)
    >>> [bugtask.status.title for bugtask in bugtasks]
    ['Unknown', 'Invalid']
    >>> bug_link_question = questionset.get(11)
    >>> bug_link_question.linkBug(fixed_bug)
    <QuestionBug at ...>

    # A question linked to an Invalid bug; it is expirable.
    >>> invalid_bug = getUtility(IBugSet).get(10)
    >>> bugtask = invalid_bug.bugtasks[0]
    >>> bugtask.transitionToStatus(BugTaskStatus.INVALID, no_priv)
    >>> bugtask.status.title
    'Invalid'
    >>> invalid_bug_question = questionset.get(12)
    >>> invalid_bug_question.linkBug(invalid_bug)
    <QuestionBug at ...>

    # Commit the current transaction because the script will run in
    # another transaction and thus it won't see the changes done on this
    # test unless we commit.
    # XXX flacoste 2006-10-03 bug=3989: Unecessary flush_database_updates
    # required.
    >>> from canonical.database.sqlbase import flush_database_updates
    >>> flush_database_updates()
    >>> import transaction
    >>> transaction.commit()

    # Run the script.
    >>> import subprocess
    >>> process = subprocess.Popen(
    ...     'cronscripts/expire-questions.py', shell=True,
    ...     stdin=subprocess.PIPE, stdout=subprocess.PIPE,
    ...     stderr=subprocess.PIPE)
    >>> (out, err) = process.communicate()
    >>> print err
    INFO    Creating lockfile: /var/lock/launchpad-expire-questions.lock
    INFO    Expiring OPEN and NEEDSINFO questions without activity for the
            last 15 days.
    INFO    Found 5 questions to expire.
    INFO    Expired 5 questions.
    INFO    Finished expiration run.
    <BLANKLINE>
    >>> print out
    <BLANKLINE>
    >>> process.returncode
    0

    # Now we flush the caches, so that the above defined objects gets
    # their content from the modified DB.
    >>> from canonical.database.sqlbase import flush_database_caches
    >>> flush_database_caches()

The status of the OPEN and NEEDSINFO questions that had recent activity
wasn't modified by the script:

    >>> print recent_open_question.status.title
    Open
    >>> print recent_needsinfo_question.status.title
    Needs information

Neither the old one which was assigned to Foo Bar:

    >>> print old_assigned_open_question.status.title
    Open

The old question with non-Invalid bug link is still Open status:

    >>> print bug_link_question.status.title
    Open

But the other ones status was changed to 'Expired':

    >>> print old_needs_info_question.status.title
    Expired
    >>> print old_open_question.status.title
    Expired
    >>> print invalid_bug_question.status.title
    Expired

The message explaining the reason for the expiration was posted by the
Launchpad Janitor celebrity:

    >>> expiration_message = old_needs_info_question.messages[-1]
    >>> print expiration_message.action.name
    EXPIRE
    >>> print expiration_message.new_status.title
    Expired
    >>> print expiration_message.owner.name
    janitor

    >>> print expiration_message.text_contents
    This question was expired because it remained in the
    'Needs information' state without activity for the last 15 days.