~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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# Copyright 2011 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""Job classes related to QuestionJob."""

__metaclass__ = type
__all__ = [
    'QuestionJob',
    ]

from lazr.delegates import delegates
import simplejson
from storm.expr import And
from storm.locals import (
    Int,
    Reference,
    Unicode,
    )
from zope.component import getUtility
from zope.interface import (
    classProvides,
    implements,
    )

from canonical.config import config
from canonical.database.enumcol import EnumCol
from canonical.launchpad.interfaces.lpstorm import IMasterStore
from canonical.launchpad.scripts import log
from lp.answers.enums import (
    QuestionJobType,
    QuestionRecipientSet,
    )
from lp.answers.interfaces.questionjob import (
    IQuestionEmailJob,
    IQuestionEmailJobSource,
    IQuestionJob,
    )
from lp.answers.model.question import Question
from lp.registry.interfaces.person import IPersonSet
from lp.services.database.stormbase import StormBase
from lp.services.job.model.job import Job
from lp.services.job.runner import BaseRunnableJob
from lp.services.mail.mailwrapper import MailWrapper
from lp.services.mail.notificationrecipientset import NotificationRecipientSet
from lp.services.mail.sendmail import (
    format_address,
    format_address_for_person,
    simple_sendmail,
    )
from lp.services.propertycache import cachedproperty


class QuestionJob(StormBase):
    """A Job for queued question emails."""

    implements(IQuestionJob)

    __storm_table__ = 'QuestionJob'

    id = Int(primary=True)

    job_id = Int(name='job')
    job = Reference(job_id, Job.id)

    job_type = EnumCol(enum=QuestionJobType, notNull=True)

    question_id = Int(name='question')
    question = Reference(question_id, Question.id)

    _json_data = Unicode('json_data')

    def __init__(self, question, job_type, metadata):
        """Constructor.

        :param question: The question related to this job.
        :param job_type: The specific job being performed for the question.
        :param metadata: The type-specific variables, as a JSON-compatible
            dict.
        """
        super(QuestionJob, self).__init__()
        self.job = Job()
        self.job_type = job_type
        self.question = question
        json_data = simplejson.dumps(metadata)
        self._json_data = json_data.decode('utf-8')

    def __repr__(self):
        return (
            "<{self.__class__.__name__} for question {self.question.id}; "
            "status={self.job.status}>").format(self=self)

    @property
    def metadata(self):
        """See `IQuestionJob`."""
        return simplejson.loads(self._json_data)


class QuestionEmailJob(BaseRunnableJob):
    """Intermediate class for deriving from QuestionJob."""

    delegates(IQuestionJob)
    implements(IQuestionEmailJob)
    classProvides(IQuestionEmailJobSource)

    def __init__(self, job):
        self.context = job

    class_job_type = QuestionJobType.EMAIL

    @classmethod
    def create(cls, question, user, recipient_set, subject, body, headers):
        """See `IQuestionJob`."""
        metadata = {
            'user': user.id,
            'recipient_set': recipient_set.name,
            'subject': subject,
            'body': body,
            'headers': headers,
            }
        job = QuestionJob(
            question=question, job_type=cls.class_job_type, metadata=metadata)
        return cls(job)

    @classmethod
    def iterReady(cls):
        """See `IJobSource`."""
        store = IMasterStore(QuestionJob)
        jobs = store.find(
            QuestionJob,
            And(QuestionJob.job_type == cls.class_job_type,
                QuestionJob.job_id.is_in(Job.ready_jobs)))
        return (cls(job) for job in jobs)

    @cachedproperty
    def user(self):
        """See `IQuestionEmailJob`."""
        return getUtility(IPersonSet).get(self.metadata['user'])

    @property
    def subject(self):
        """See `IQuestionEmailJob`."""
        return self.metadata['subject']

    @property
    def body(self):
        """See `IQuestionEmailJob`."""
        return self.metadata['body']

    @property
    def headers(self):
        """See `IQuestionEmailJob`."""
        return self.metadata['headers']

    @property
    def log_name(self):
        """See `IRunnableJob`."""
        return self.__class__.__name__

    def getOopsVars(self):
        """See `IRunnableJob`."""
        vars = BaseRunnableJob.getOopsVars(self)
        vars.extend([
            ('question', self.question.id),
            ('user', self.user.name),
            ])
        return vars

    def getErrorRecipients(self):
        """See `IRunnableJob`."""
        return [format_address_for_person(self.user)]

    @property
    def from_address(self):
        """See `IQuestionEmailJob`."""
        address = 'question%s@%s' % (
            self.question.id, config.answertracker.email_domain)
        return format_address(self.user.displayname, address)

    @property
    def recipients(self):
        """See `IQuestionEmailJob`."""
        term = QuestionRecipientSet.getTermByToken(
            self.metadata['recipient_set'])
        question_recipient_set = term.value
        if question_recipient_set == QuestionRecipientSet.ASKER:
            recipients = NotificationRecipientSet()
            owner = self.question.owner
            original_recipients = self.question.direct_recipients
            if owner in original_recipients:
                rationale, header = original_recipients.getReason(owner)
                recipients.add(owner, rationale, header)
            return recipients
        elif question_recipient_set == QuestionRecipientSet.SUBSCRIBER:
            recipients = self.question.getRecipients()
            if self.question.owner in recipients:
                recipients.remove(self.question.owner)
            return recipients
        elif question_recipient_set == QuestionRecipientSet.ASKER_SUBSCRIBER:
            return self.question.getRecipients()
        elif question_recipient_set == QuestionRecipientSet.CONTACT:
            return self.question.target.getAnswerContactRecipients(None)
        else:
            raise ValueError(
                'Unsupported QuestionRecipientSet value: %s' %
                question_recipient_set)

    def buildBody(self, rationale):
        """See `IQuestionEmailJob`."""
        wrapper = MailWrapper()
        body_parts = [self.body, wrapper.format(rationale)]
        if '\n-- ' not in self.body:
            body_parts.insert(1, '-- ')
        return '\n'.join(body_parts)

    def run(self):
        """See `IRunnableJob`.

        Send emails to all the question recipients.
        """
        log.debug(
            "%s will send email for question %s.",
            self.log_name, self.question.id)
        headers = self.headers
        recipients = self.recipients
        for email in recipients.getEmails():
            rationale, header = recipients.getReason(email)
            headers['X-Launchpad-Message-Rationale'] = header
            formatted_body = self.buildBody(rationale)
            simple_sendmail(
                self.from_address, email, self.subject, formatted_body,
                headers)
        log.debug(
            "%s has sent email for question %s.",
            self.log_name, self.question.id)