1 | # -*- coding: utf-8 -*- |
---|
2 | # |
---|
3 | # Copyright (c) 2008, Stephen Hansen |
---|
4 | # Copyright (c) 2009, Robert Corsaro |
---|
5 | # Copyright (c) 2010-2012, Steffen Hoffmann |
---|
6 | # |
---|
7 | # This software is licensed as described in the file COPYING, which |
---|
8 | # you should have received as part of this distribution. |
---|
9 | # |
---|
10 | |
---|
11 | import time |
---|
12 | |
---|
13 | from operator import itemgetter |
---|
14 | from pkg_resources import resource_filename |
---|
15 | |
---|
16 | from trac import __version__ as trac_version |
---|
17 | from trac.config import ExtensionOption |
---|
18 | from trac.core import Component, ExtensionPoint, Interface, TracError, \ |
---|
19 | implements |
---|
20 | from trac.db import DatabaseManager |
---|
21 | from trac.env import IEnvironmentSetupParticipant |
---|
22 | |
---|
23 | from announcer import db_default |
---|
24 | |
---|
25 | |
---|
26 | class IAnnouncementProducer(Interface): |
---|
27 | """Producer converts Trac events from different subsystems, into |
---|
28 | AnnouncerEvents. |
---|
29 | """ |
---|
30 | |
---|
31 | def realms(): |
---|
32 | """Returns an iterable that lists all the realms that this producer |
---|
33 | is capable of producing events for. |
---|
34 | """ |
---|
35 | |
---|
36 | |
---|
37 | class IAnnouncementSubscriber(Interface): |
---|
38 | """IAnnouncementSubscriber provides an interface where a Plug-In can |
---|
39 | register realms and categories of subscriptions it is able to provide. |
---|
40 | |
---|
41 | An IAnnouncementSubscriber component can use any means to determine |
---|
42 | if a user is interested in hearing about a given event. More then one |
---|
43 | component can handle the same realms and categories. |
---|
44 | |
---|
45 | The subscriber must also indicate not just that a user is interested |
---|
46 | in receiving a particular notice. Again, how it makes that decision is |
---|
47 | entirely up to a particular implementation.""" |
---|
48 | |
---|
49 | def matches(event): |
---|
50 | """Returns a list of subscriptions that match the given event. |
---|
51 | Responses should be yielded as 7 part tuples as follows: |
---|
52 | (distributor, sid, authenticated, address, format, priority, adverb) |
---|
53 | The default installation includes email and xmpp distributors. The |
---|
54 | default installation includes formats for text/plain and text/html. |
---|
55 | If an unknown format is return, it will be replaced by a default known |
---|
56 | format. Priority is used to resolve conflicting subscriptions for the |
---|
57 | same user/distribution pair. adverb is either always or never. |
---|
58 | """ |
---|
59 | |
---|
60 | def description(): |
---|
61 | """A description of the subscription that shows up in the users |
---|
62 | preferences. |
---|
63 | """ |
---|
64 | |
---|
65 | def requires_authentication(): |
---|
66 | """Returns True or False. If the user is required to be authenticated |
---|
67 | to create the subscription, then return True. This applies to things |
---|
68 | like ticket owner subscriber, since the ticket owner can never be the |
---|
69 | sid of an unauthenticated user and we have no way to lookup users by |
---|
70 | email address (as of yet). |
---|
71 | """ |
---|
72 | |
---|
73 | |
---|
74 | class IAnnouncementDefaultSubscriber(Interface): |
---|
75 | """Default subscriptions that the module will automatically generate. |
---|
76 | This should only be used in reasonable situations, where users can be |
---|
77 | determined by the event itself. For instance, ticket author has a |
---|
78 | default subscription that is controlled via trac.ini. This is because |
---|
79 | we can lookup the ticket author during the event and create a |
---|
80 | subscription for them. Default subscriptions should be low priority |
---|
81 | so that the user can easily override them. |
---|
82 | """ |
---|
83 | |
---|
84 | def default_subscriptions(): |
---|
85 | """Yields 5 part tuple containing (class, distributor, priority, |
---|
86 | adverb). This is used to display default subscriptions in the |
---|
87 | user UI and can also be used by matches to figure out what |
---|
88 | default subscriptions it should yield. |
---|
89 | """ |
---|
90 | |
---|
91 | |
---|
92 | class IAnnouncementSubscriptionFilter(Interface): |
---|
93 | """IAnnouncementSubscriptionFilter provides an interface where a component |
---|
94 | can filter subscribers from the final distribution list. |
---|
95 | """ |
---|
96 | |
---|
97 | def filter_subscriptions(event, subscriptions): |
---|
98 | """Returns a filtered iterator of subscriptions. This method is called |
---|
99 | after all get_subscriptions_for_event calls are made to allow |
---|
100 | components to remove addresses from the distribution list. This can |
---|
101 | be used for things like "never notify updater" functionality. |
---|
102 | """ |
---|
103 | |
---|
104 | |
---|
105 | class IAnnouncementFormatter(Interface): |
---|
106 | """Formatters are responsible for converting an event into a message |
---|
107 | appropriate for a given transport. |
---|
108 | |
---|
109 | For transports like 'aim' or 'irc', this may be a short summary of a |
---|
110 | change. For 'email', it may be a plaintext or html overview of all |
---|
111 | the changes and perhaps the existing state. |
---|
112 | |
---|
113 | It's up to a formatter to determine what ends up ultimately being sent |
---|
114 | to the end-user. It's capable of pulling data out of the target object |
---|
115 | that wasn't changed, picking and choosing details for whatever reason. |
---|
116 | |
---|
117 | Since a formatter must be intimately familiar with the realm that |
---|
118 | originated the event, formatters are tied to specific transport + realm |
---|
119 | combinations. This means there may be a proliferation of formatters as |
---|
120 | options expand. |
---|
121 | """ |
---|
122 | |
---|
123 | def format_styles(transport, realm): |
---|
124 | """Returns an iterable of styles that this formatter supports for |
---|
125 | a specified transport and realm. |
---|
126 | |
---|
127 | Many formatters may simply return a single style and never have more; |
---|
128 | that's fine. But if its useful to encapsulate code for several similar |
---|
129 | styles a formatter can handle more then one. For example, 'text/plain' |
---|
130 | and 'text/html' may be useful variants the same formatter handles. |
---|
131 | |
---|
132 | Formatters retain the ability to descriminate by transport, but don't |
---|
133 | need to. |
---|
134 | """ |
---|
135 | |
---|
136 | def alternative_style_for(transport, realm, style): |
---|
137 | """Returns an alternative style for the given style if one is |
---|
138 | available. |
---|
139 | """ |
---|
140 | |
---|
141 | def format(transport, realm, style, event): |
---|
142 | """Converts the event into the specified style. If the transport or |
---|
143 | realm passed into this method are not ones this formatter can handle, |
---|
144 | it should return silently and without error. |
---|
145 | |
---|
146 | The exact return type of this method is intentionally undefined. It |
---|
147 | will be whatever the distributor that it is designed to work with |
---|
148 | expects. |
---|
149 | """ |
---|
150 | |
---|
151 | |
---|
152 | class IAnnouncementDistributor(Interface): |
---|
153 | """The Distributor is responsible for actually delivering an event to the |
---|
154 | desired subscriptions. |
---|
155 | |
---|
156 | A distributor should attempt to avoid blocking; using subprocesses is |
---|
157 | preferred to threads. |
---|
158 | |
---|
159 | Each distributor handles a single transport, and only one distributor |
---|
160 | in the system should handle that. For example, there should not be |
---|
161 | two distributors for the 'email' transport. |
---|
162 | """ |
---|
163 | |
---|
164 | def transports(): |
---|
165 | """Returns an iter of the transport supported.""" |
---|
166 | |
---|
167 | def distribute(transport, recipients, event): |
---|
168 | """This method is meant to actually distribute the event to the |
---|
169 | specified recipients, over the specified transport. |
---|
170 | |
---|
171 | If it is passed a transport it does not support, it should return |
---|
172 | silently and without error. |
---|
173 | |
---|
174 | The recipients is a list of (name, address) pairs with either (but not |
---|
175 | both) being allowed to be None. If name is provided but address isn't, |
---|
176 | then the distributor should defer to IAnnouncementAddressResolver |
---|
177 | implementations to determine what the address should be. |
---|
178 | |
---|
179 | If the name is None but the address is not, then the distributor |
---|
180 | should rely on the address being correct and use it-- if possible. |
---|
181 | |
---|
182 | The distributor may initiate as many transactions as are necessecary |
---|
183 | to deliver a message, but should use as few as possible; for example |
---|
184 | in the EmailDistributor, if all of the recipients are receiving a |
---|
185 | plain text form of the message, a single message with many BCC's |
---|
186 | should be used. |
---|
187 | |
---|
188 | The distributor is responsible for determining which of the |
---|
189 | IAnnouncementFormatters should get the privilege of actually turning |
---|
190 | an event into content. In cases where multiple formatters are capable |
---|
191 | of converting an event into a message for a given transport, a |
---|
192 | user preference would be a dandy idea. |
---|
193 | """ |
---|
194 | |
---|
195 | |
---|
196 | class IAnnouncementPreferenceProvider(Interface): |
---|
197 | """Represents a single 'box' in the Announcements preference panel. |
---|
198 | |
---|
199 | Any component can always implement IPreferencePanelProvider to get |
---|
200 | preferences from users, of course. However, considering there may be |
---|
201 | several components related to the Announcement system, and many may |
---|
202 | have different preferences for a user to set, that would clutter up |
---|
203 | the preference interfac quite a bit. |
---|
204 | |
---|
205 | The IAnnouncementPreferenceProvider allows several boxes to be |
---|
206 | chained in the same panel to group the preferenecs related to the |
---|
207 | Announcement System. |
---|
208 | |
---|
209 | Implementing announcement preference boxes should be essentially |
---|
210 | identical to implementing entire panels. |
---|
211 | """ |
---|
212 | |
---|
213 | def get_announcement_preference_boxes(req): |
---|
214 | """Accepts a request object, and returns an iterable of |
---|
215 | (name, label) pairs; one for each box that the implementation |
---|
216 | can generate. |
---|
217 | |
---|
218 | If a single item is returned, be sure to 'yield' it instead of |
---|
219 | returning it.""" |
---|
220 | |
---|
221 | def render_announcement_preference_box(req, box): |
---|
222 | """Accepts a request object, and the name (as from the previous |
---|
223 | method) of the box that should be rendered. |
---|
224 | |
---|
225 | Returns a tuple of (template, data) with the template being a |
---|
226 | filename in a directory provided by an ITemplateProvider which |
---|
227 | shall be rendered into a single <div> element, when combined |
---|
228 | with the data member. |
---|
229 | """ |
---|
230 | |
---|
231 | |
---|
232 | class IAnnouncementAddressResolver(Interface): |
---|
233 | """Handles mapping Trac usernames to addresses for distributors to use.""" |
---|
234 | |
---|
235 | def get_address_for_name(name, authenticated): |
---|
236 | """Accepts a session name, and returns an address. |
---|
237 | |
---|
238 | This address explicitly does not always have to mean an email address, |
---|
239 | nor does it have to be an address stored within the Trac system at |
---|
240 | all. |
---|
241 | |
---|
242 | Implementations of this interface are never 'detected' automatically, |
---|
243 | and must instead be specifically named for a particular distributor. |
---|
244 | This way, some may find email addresses (for EmailDistributor), and |
---|
245 | others may find AIM screen name. |
---|
246 | |
---|
247 | If no address for the specified name can be found, None should be |
---|
248 | returned. The next resolver will be attempted in the chain. |
---|
249 | """ |
---|
250 | |
---|
251 | |
---|
252 | class AnnouncementEvent(object): |
---|
253 | """AnnouncementEvent |
---|
254 | |
---|
255 | This packages together in a single place all data related to a particular |
---|
256 | event; notably the realm, category, and the target that represents the |
---|
257 | initiator of the event. |
---|
258 | |
---|
259 | In some (rare) cases, the target may be None; in cases where the message |
---|
260 | is all that matters and there's no possible data you could conceivably |
---|
261 | get beyond just the message. |
---|
262 | """ |
---|
263 | def __init__(self, realm, category, target, author=""): |
---|
264 | self.realm = realm |
---|
265 | self.category = category |
---|
266 | self.target = target |
---|
267 | self.author = author |
---|
268 | |
---|
269 | def get_basic_terms(self): |
---|
270 | return (self.realm, self.category) |
---|
271 | |
---|
272 | def get_session_terms(self, session_id): |
---|
273 | return tuple() |
---|
274 | |
---|
275 | |
---|
276 | class IAnnouncementSubscriptionResolver(Interface): |
---|
277 | """Supports new and old style of subscription resolution until new code |
---|
278 | is complete.""" |
---|
279 | |
---|
280 | def subscriptions(event): |
---|
281 | """Return all subscriptions as (dist, sid, auth, address, format) |
---|
282 | priority 1 is highest. adverb is 'always' or 'never'. |
---|
283 | """ |
---|
284 | |
---|
285 | |
---|
286 | class SubscriptionResolver(Component): |
---|
287 | """Collect, and resolve subscriptions.""" |
---|
288 | |
---|
289 | implements(IAnnouncementSubscriptionResolver) |
---|
290 | |
---|
291 | subscribers = ExtensionPoint(IAnnouncementSubscriber) |
---|
292 | |
---|
293 | def subscriptions(self, event): |
---|
294 | """Yields all subscriptions for a given event.""" |
---|
295 | |
---|
296 | subscriptions = [] |
---|
297 | for sp in self.subscribers: |
---|
298 | subscriptions.extend( |
---|
299 | [x for x in sp.matches(event) if x] |
---|
300 | ) |
---|
301 | |
---|
302 | """ |
---|
303 | This logic is meant to generate a list of subscriptions for each |
---|
304 | distribution method. The important thing is, that we pick the rule |
---|
305 | with the highest priority for each (sid, distribution) pair. |
---|
306 | If it is "never", then the user is dropped from the list, |
---|
307 | if it is "always", then the user is kept. |
---|
308 | Only users highest priority rule is used and all others are skipped. |
---|
309 | """ |
---|
310 | # sort by dist, sid, authenticated, priority |
---|
311 | subscriptions.sort(key=lambda i:(i[1],i[2],i[3],i[6])) |
---|
312 | |
---|
313 | resolved_subs = [] |
---|
314 | |
---|
315 | # collect highest priority for each (sid, dist) pair |
---|
316 | state = { |
---|
317 | 'last': None |
---|
318 | } |
---|
319 | for s in subscriptions: |
---|
320 | if (s[1], s[2], s[3]) == state['last']: |
---|
321 | continue |
---|
322 | if s[-1] == 'always': |
---|
323 | self.log.debug("Adding (%s [%s]) for 'always' on rule (%s) " |
---|
324 | "for (%s)"%(s[2], s[3], s[0], s[1])) |
---|
325 | resolved_subs.append(s[1:6]) |
---|
326 | else: |
---|
327 | self.log.debug("Ignoring (%s [%s]) for 'never' on rule (%s) " |
---|
328 | "for (%s)"%(s[2], s[3], s[0], s[1])) |
---|
329 | |
---|
330 | # if s[1] is None, then the subscription is for a raw email |
---|
331 | # address that has been set in some field and we shouldn't skip |
---|
332 | # the next raw email subscription. In other words, all raw email |
---|
333 | # subscriptions should be added. |
---|
334 | if s[2]: |
---|
335 | state['last'] = (s[1], s[2], s[3]) |
---|
336 | |
---|
337 | return resolved_subs |
---|
338 | |
---|
339 | |
---|
340 | _TRUE_VALUES = ('yes', 'true', 'enabled', 'on', 'aye', '1', 1, True) |
---|
341 | |
---|
342 | def istrue(value, otherwise=False): |
---|
343 | return True and (value in _TRUE_VALUES) or otherwise |
---|
344 | |
---|
345 | |
---|
346 | # Import i18n methods. Fallback modules maintain compatibility to Trac 0.11 |
---|
347 | # by keeping Babel optional here. |
---|
348 | try: |
---|
349 | from trac.util.translation import domain_functions |
---|
350 | add_domain, _, N_ , tag_= \ |
---|
351 | domain_functions('announcer', ('add_domain', '_', 'N_', 'tag_')) |
---|
352 | except ImportError: |
---|
353 | from genshi.builder import tag as tag_ |
---|
354 | from trac.util.translation import gettext |
---|
355 | _ = gettext |
---|
356 | N_ = lambda text: text |
---|
357 | def add_domain(a, b, c=None): |
---|
358 | pass |
---|
359 | |
---|
360 | |
---|
361 | class AnnouncementSystem(Component): |
---|
362 | """AnnouncementSystem represents the entry-point into the announcement |
---|
363 | system, and is also the central controller that handles passing notices |
---|
364 | around. |
---|
365 | |
---|
366 | An announcement begins when something-- an announcement provider-- |
---|
367 | constructs an AnnouncementEvent (or subclass) and calls the send method |
---|
368 | on the AnnouncementSystem. |
---|
369 | |
---|
370 | Every event is classified by two required fields-- realm and category. |
---|
371 | In general, the realm corresponds to the realm of a Resource within Trac; |
---|
372 | ticket, wiki, milestone, and such. This is not a requirement, however. |
---|
373 | Realms can be anything distinctive-- if you specify novel realms to solve |
---|
374 | a particular problem, you'll simply also have to specify subscribers and |
---|
375 | formatters who are able to deal with data in those realms. |
---|
376 | |
---|
377 | The other classifier is a category that is defined by the providers and |
---|
378 | has no particular meaning; for the providers that implement the |
---|
379 | I*ChangeListener interfaces, the categories will often correspond to the |
---|
380 | kinds of events they receive. For tickets, they would be 'created', |
---|
381 | 'changed' and 'deleted'. |
---|
382 | |
---|
383 | There is no requirement for an event to have more then realm and category |
---|
384 | to classify an event, but if more is provided in a subclass that the |
---|
385 | subscribers can use to pick through events, all power to you. |
---|
386 | """ |
---|
387 | |
---|
388 | implements(IEnvironmentSetupParticipant) |
---|
389 | |
---|
390 | subscribers = ExtensionPoint(IAnnouncementSubscriber) |
---|
391 | subscription_filters = ExtensionPoint(IAnnouncementSubscriptionFilter) |
---|
392 | subscription_resolvers = ExtensionPoint(IAnnouncementSubscriptionResolver) |
---|
393 | distributors = ExtensionPoint(IAnnouncementDistributor) |
---|
394 | |
---|
395 | resolver = ExtensionOption('announcer', 'subscription_resolvers', |
---|
396 | IAnnouncementSubscriptionResolver, 'SubscriptionResolver', |
---|
397 | """Comma-separated list of subscription resolver components in the |
---|
398 | order they will be called. |
---|
399 | """) |
---|
400 | |
---|
401 | def __init__(self): |
---|
402 | # Bind the 'announcer' catalog to the specified locale directory. |
---|
403 | locale_dir = resource_filename(__name__, 'locale') |
---|
404 | add_domain(self.env.path, locale_dir) |
---|
405 | |
---|
406 | # IEnvironmentSetupParticipant methods |
---|
407 | |
---|
408 | def environment_created(self): |
---|
409 | self._upgrade_db(self.env.get_db_cnx()) |
---|
410 | |
---|
411 | def environment_needs_upgrade(self, db): |
---|
412 | schema_ver = self.get_schema_version(db) |
---|
413 | if schema_ver == db_default.schema_version: |
---|
414 | return False |
---|
415 | if schema_ver > db_default.schema_version: |
---|
416 | raise TracError(_("""A newer plugin version has been installed |
---|
417 | before, but downgrading is unsupported.""")) |
---|
418 | self.log.info("TracAnnouncer db schema version is %d, should be %d" |
---|
419 | % (schema_ver, db_default.schema_version)) |
---|
420 | return True |
---|
421 | |
---|
422 | def upgrade_environment(self, db): |
---|
423 | self._upgrade_db(db) |
---|
424 | |
---|
425 | # Internal methods |
---|
426 | |
---|
427 | def get_schema_version(self, db=None): |
---|
428 | """Return the current schema version for this plugin.""" |
---|
429 | db = db and db or self.env.get_db_cnx() |
---|
430 | cursor = db.cursor() |
---|
431 | cursor.execute(""" |
---|
432 | SELECT value |
---|
433 | FROM system |
---|
434 | WHERE name='announcer_version' |
---|
435 | """) |
---|
436 | row = cursor.fetchone() |
---|
437 | if not (row and int(row[0]) > 5): |
---|
438 | # Care for pre-announcer-1.0 installations. |
---|
439 | dburi = self.config.get('trac', 'database') |
---|
440 | tables = self._get_tables(dburi, cursor) |
---|
441 | if 'subscription' in tables: |
---|
442 | # Version > 2 |
---|
443 | cursor.execute("SELECT * FROM subscription_attribute") |
---|
444 | columns = [col[0] for col in |
---|
445 | self._get_cursor_description(cursor)] |
---|
446 | if 'authenticated' in columns: |
---|
447 | self.env.log.debug( |
---|
448 | "TracAnnouncer needs to register schema version") |
---|
449 | return 5 |
---|
450 | if 'realm' in columns: |
---|
451 | self.env.log.debug( |
---|
452 | "TracAnnouncer needs to change a table") |
---|
453 | return 4 |
---|
454 | self.env.log.debug("TracAnnouncer needs to change tables") |
---|
455 | return 3 |
---|
456 | if 'subscriptions' in tables: |
---|
457 | cursor.execute("SELECT * FROM subscriptions") |
---|
458 | columns = [col[0] for col in |
---|
459 | self._get_cursor_description(cursor)] |
---|
460 | if not 'format' in columns: |
---|
461 | self.env.log.debug("TracAnnouncer needs to add new tables") |
---|
462 | return 2 |
---|
463 | self.env.log.debug("TracAnnouncer needs to add/change tables") |
---|
464 | return 1 |
---|
465 | # This is a new installation. |
---|
466 | return 0 |
---|
467 | # The expected outcome for any up-to-date installation. |
---|
468 | return row and int(row[0]) or 0 |
---|
469 | |
---|
470 | def _get_cursor_description(self, cursor): |
---|
471 | # Cursors don't look the same across Trac versions |
---|
472 | if trac_version < '0.12': |
---|
473 | return cursor.description |
---|
474 | else: |
---|
475 | return cursor.cursor.description |
---|
476 | |
---|
477 | def _get_tables(self, dburi, cursor): |
---|
478 | """Code from TracMigratePlugin by Jun Omae (see tracmigrate.admin).""" |
---|
479 | if dburi.startswith('sqlite:'): |
---|
480 | sql = """ |
---|
481 | SELECT name |
---|
482 | FROM sqlite_master |
---|
483 | WHERE type='table' |
---|
484 | AND NOT name='sqlite_sequence' |
---|
485 | """ |
---|
486 | elif dburi.startswith('postgres:'): |
---|
487 | sql = """ |
---|
488 | SELECT tablename |
---|
489 | FROM pg_tables |
---|
490 | WHERE schemaname = ANY (current_schemas(false)) |
---|
491 | """ |
---|
492 | elif dburi.startswith('mysql:'): |
---|
493 | sql = "SHOW TABLES" |
---|
494 | else: |
---|
495 | raise TracError('Unsupported database type "%s"' |
---|
496 | % dburi.split(':')[0]) |
---|
497 | cursor.execute(sql) |
---|
498 | return sorted([row[0] for row in cursor]) |
---|
499 | |
---|
500 | def _upgrade_db(self, db): |
---|
501 | """Each schema version should have its own upgrade module, named |
---|
502 | upgrades/dbN.py, where 'N' is the version number (int). |
---|
503 | """ |
---|
504 | db_mgr = DatabaseManager(self.env) |
---|
505 | schema_ver = self.get_schema_version(db) |
---|
506 | |
---|
507 | cursor = db.cursor() |
---|
508 | # Is this a new installation? |
---|
509 | if not schema_ver: |
---|
510 | # Perform a single-step install: Create plugin schema and |
---|
511 | # insert default data into the database. |
---|
512 | connector = db_mgr._get_connector()[0] |
---|
513 | for table in db_default.schema: |
---|
514 | for stmt in connector.to_sql(table): |
---|
515 | cursor.execute(stmt) |
---|
516 | for table, cols, vals in db_default.get_data(db): |
---|
517 | cursor.executemany("INSERT INTO %s (%s) VALUES (%s)" % (table, |
---|
518 | ','.join(cols), |
---|
519 | ','.join(['%s' for c in cols])), vals) |
---|
520 | else: |
---|
521 | # Perform incremental upgrades. |
---|
522 | for i in range(schema_ver + 1, db_default.schema_version + 1): |
---|
523 | name = 'db%i' % i |
---|
524 | try: |
---|
525 | upgrades = __import__('announcer.upgrades', globals(), |
---|
526 | locals(), [name]) |
---|
527 | script = getattr(upgrades, name) |
---|
528 | except AttributeError: |
---|
529 | raise TracError(_(""" |
---|
530 | No upgrade module for version %(num)i (%(version)s.py) |
---|
531 | """, num=i, version=name)) |
---|
532 | script.do_upgrade(self.env, i, cursor) |
---|
533 | cursor.execute(""" |
---|
534 | UPDATE system |
---|
535 | SET value=%s |
---|
536 | WHERE name='announcer_version' |
---|
537 | """, (db_default.schema_version,)) |
---|
538 | self.log.info("Upgraded TracAnnouncer db schema from version %d to %d" |
---|
539 | % (schema_ver, db_default.schema_version)) |
---|
540 | db.commit() |
---|
541 | |
---|
542 | # AnnouncementSystem core methods |
---|
543 | |
---|
544 | def send(self, evt): |
---|
545 | start = time.time() |
---|
546 | self._real_send(evt) |
---|
547 | stop = time.time() |
---|
548 | self.log.debug("AnnouncementSystem sent event in %s seconds." |
---|
549 | % (round(stop - start, 2))) |
---|
550 | |
---|
551 | def _real_send(self, evt): |
---|
552 | """Accepts a single AnnouncementEvent instance (or subclass), and |
---|
553 | returns nothing. |
---|
554 | |
---|
555 | There is no way (intentionally) to determine what the |
---|
556 | AnnouncementSystem did with a particular event besides looking through |
---|
557 | the debug logs. |
---|
558 | """ |
---|
559 | try: |
---|
560 | subscriptions = self.resolver.subscriptions(evt) |
---|
561 | for sf in self.subscription_filters: |
---|
562 | subscriptions = set( |
---|
563 | sf.filter_subscriptions(evt, subscriptions) |
---|
564 | ) |
---|
565 | |
---|
566 | self.log.debug( |
---|
567 | "AnnouncementSystem has found the following subscriptions: " \ |
---|
568 | "%s"%(', '.join(['[%s(%s) via %s]' % ((s[1] or s[3]),\ |
---|
569 | s[2] and 'authenticated' or 'not authenticated',s[0])\ |
---|
570 | for s in subscriptions] |
---|
571 | ) |
---|
572 | ) |
---|
573 | ) |
---|
574 | packages = {} |
---|
575 | for transport, sid, authenticated, address, subs_format \ |
---|
576 | in subscriptions: |
---|
577 | if transport not in packages: |
---|
578 | packages[transport] = set() |
---|
579 | packages[transport].add((sid,authenticated,address)) |
---|
580 | for distributor in self.distributors: |
---|
581 | for transport in distributor.transports(): |
---|
582 | if transport in packages: |
---|
583 | distributor.distribute(transport, packages[transport], |
---|
584 | evt) |
---|
585 | except: |
---|
586 | self.log.error("AnnouncementSystem failed.", exc_info=True) |
---|