0
# Django Signals
1
2
Event system for extending Rosetta functionality and integrating with custom workflows through Django's signal framework. These signals provide hooks for monitoring translation activities, implementing custom business logic, and integrating with external systems.
3
4
## Capabilities
5
6
### Translation Entry Change Signal
7
8
Signal fired when individual translation entries are modified, providing detailed information about the change.
9
10
```python { .api }
11
entry_changed = django.dispatch.Signal()
12
"""
13
Signal fired when translation entry is modified.
14
15
Sent whenever a translation entry (msgstr) is changed in the web interface,
16
allowing custom code to respond to translation modifications.
17
18
Signal Arguments:
19
- sender: The view class that triggered the change
20
- user: Django User instance who made the change
21
- old_msgstr: Previous translation string value
22
- old_fuzzy: Previous fuzzy flag state (boolean)
23
- pofile: POFile object representing the .po file
24
- language_code: Language code of the modified translation
25
- request: Django HttpRequest instance
26
27
Usage:
28
@receiver(entry_changed)
29
def handle_entry_changed(sender, **kwargs):
30
user = kwargs['user']
31
old_msgstr = kwargs['old_msgstr']
32
language_code = kwargs['language_code']
33
# Custom logic here
34
"""
35
36
@receiver(entry_changed)
37
def log_translation_changes(sender, **kwargs):
38
"""
39
Example signal handler for logging translation changes.
40
41
Parameters from signal:
42
- sender: View class
43
- user: User who made the change
44
- old_msgstr: Previous translation
45
- old_fuzzy: Previous fuzzy state
46
- pofile: POFile object
47
- language_code: Language being translated
48
- request: HTTP request
49
"""
50
```
51
52
### Post-Save Signal
53
54
Signal fired after .po files are saved to disk, enabling post-processing workflows.
55
56
```python { .api }
57
post_save = django.dispatch.Signal()
58
"""
59
Signal fired after .po file is saved to disk.
60
61
Sent after translation changes are committed to the .po file, allowing
62
custom code to perform post-processing, notifications, or integrations.
63
64
Signal Arguments:
65
- sender: The view class that saved the file
66
- language_code: Language code of the saved file
67
- request: Django HttpRequest instance
68
69
Usage:
70
@receiver(post_save)
71
def handle_po_file_saved(sender, **kwargs):
72
language_code = kwargs['language_code']
73
request = kwargs['request']
74
# Custom post-save logic here
75
"""
76
77
@receiver(post_save)
78
def compile_mo_files(sender, **kwargs):
79
"""
80
Example signal handler for compiling .mo files after .po save.
81
82
Parameters from signal:
83
- sender: View class
84
- language_code: Language that was saved
85
- request: HTTP request
86
"""
87
```
88
89
## Usage Examples
90
91
### Basic Signal Handlers
92
93
```python
94
# myapp/signals.py
95
from django.dispatch import receiver
96
from rosetta.signals import entry_changed, post_save
97
import logging
98
99
logger = logging.getLogger('translation_activity')
100
101
@receiver(entry_changed)
102
def log_translation_activity(sender, **kwargs):
103
"""Log all translation changes for audit purposes."""
104
105
user = kwargs['user']
106
old_msgstr = kwargs['old_msgstr']
107
language_code = kwargs['language_code']
108
pofile = kwargs['pofile']
109
110
# Find the changed entry (simplified example)
111
for entry in pofile:
112
if entry.msgstr != old_msgstr and entry.msgstr: # New translation
113
logger.info(
114
f"Translation changed by {user.username}: "
115
f"'{entry.msgid}' -> '{entry.msgstr}' ({language_code})"
116
)
117
break
118
119
@receiver(post_save)
120
def notify_translation_team(sender, **kwargs):
121
"""Send notifications when translations are saved."""
122
123
language_code = kwargs['language_code']
124
request = kwargs['request']
125
126
# Send email notification (example)
127
from django.core.mail import send_mail
128
129
send_mail(
130
subject=f'Translations updated for {language_code}',
131
message=f'User {request.user.username} has updated {language_code} translations.',
132
from_email='noreply@example.com',
133
recipient_list=['translation-team@example.com'],
134
fail_silently=True
135
)
136
```
137
138
### Integration with Version Control
139
140
```python
141
# myapp/signals.py
142
from django.dispatch import receiver
143
from rosetta.signals import post_save
144
import subprocess
145
import os
146
147
@receiver(post_save)
148
def auto_commit_translations(sender, **kwargs):
149
"""Automatically commit translation changes to git."""
150
151
language_code = kwargs['language_code']
152
request = kwargs['request']
153
154
try:
155
# Navigate to project root
156
project_root = '/path/to/your/project'
157
os.chdir(project_root)
158
159
# Add all .po files
160
subprocess.run(['git', 'add', '*.po'], check=True)
161
162
# Commit with descriptive message
163
commit_message = f"Update {language_code} translations by {request.user.username}"
164
subprocess.run(['git', 'commit', '-m', commit_message], check=True)
165
166
logger.info(f"Auto-committed {language_code} translations")
167
168
except subprocess.CalledProcessError as e:
169
logger.error(f"Failed to auto-commit translations: {e}")
170
171
@receiver(entry_changed)
172
def track_translation_quality(sender, **kwargs):
173
"""Track translation quality metrics."""
174
175
user = kwargs['user']
176
old_msgstr = kwargs['old_msgstr']
177
old_fuzzy = kwargs['old_fuzzy']
178
pofile = kwargs['pofile']
179
180
# Find the changed entry
181
for entry in pofile:
182
if hasattr(entry, 'msgstr') and entry.msgstr != old_msgstr:
183
# Track quality changes
184
if old_fuzzy and not entry.fuzzy:
185
# Fuzzy entry was clarified
186
record_quality_improvement(user, entry, 'fuzzy_resolved')
187
elif not old_msgstr and entry.msgstr:
188
# New translation added
189
record_quality_improvement(user, entry, 'translation_added')
190
elif old_msgstr and not entry.msgstr:
191
# Translation removed
192
record_quality_decline(user, entry, 'translation_removed')
193
break
194
195
def record_quality_improvement(user, entry, action_type):
196
"""Record translation quality improvements."""
197
# Implementation depends on your quality tracking system
198
pass
199
200
def record_quality_decline(user, entry, action_type):
201
"""Record translation quality declines."""
202
# Implementation depends on your quality tracking system
203
pass
204
```
205
206
### Custom Workflow Integration
207
208
```python
209
# myapp/signals.py
210
from django.dispatch import receiver
211
from rosetta.signals import entry_changed, post_save
212
from myapp.models import TranslationWorkflow, TranslationReview
213
214
@receiver(entry_changed)
215
def trigger_review_workflow(sender, **kwargs):
216
"""Trigger review workflow for translation changes."""
217
218
user = kwargs['user']
219
language_code = kwargs['language_code']
220
pofile = kwargs['pofile']
221
222
# Create review request for significant changes
223
workflow, created = TranslationWorkflow.objects.get_or_create(
224
language_code=language_code,
225
status='in_progress'
226
)
227
228
# Add review item
229
TranslationReview.objects.create(
230
workflow=workflow,
231
translator=user,
232
po_file_path=pofile.fpath,
233
status='pending_review'
234
)
235
236
@receiver(post_save)
237
def update_translation_cache(sender, **kwargs):
238
"""Update cached translation statistics."""
239
240
language_code = kwargs['language_code']
241
242
# Clear relevant caches
243
from django.core.cache import cache
244
cache_keys = [
245
f'translation_stats_{language_code}',
246
f'translation_progress_{language_code}',
247
'translation_overview'
248
]
249
250
for key in cache_keys:
251
cache.delete(key)
252
253
# Regenerate statistics asynchronously
254
from myapp.tasks import update_translation_statistics
255
update_translation_statistics.delay(language_code)
256
257
@receiver(post_save)
258
def backup_translations(sender, **kwargs):
259
"""Create backup of translation files."""
260
261
language_code = kwargs['language_code']
262
request = kwargs['request']
263
264
from myapp.utils import create_translation_backup
265
266
try:
267
backup_path = create_translation_backup(language_code)
268
logger.info(f"Created translation backup: {backup_path}")
269
270
# Optionally store backup metadata
271
from myapp.models import TranslationBackup
272
TranslationBackup.objects.create(
273
language_code=language_code,
274
backup_path=backup_path,
275
created_by=request.user,
276
reason='auto_save'
277
)
278
279
except Exception as e:
280
logger.error(f"Failed to create translation backup: {e}")
281
```
282
283
### Metrics and Analytics
284
285
```python
286
# myapp/signals.py
287
from django.dispatch import receiver
288
from rosetta.signals import entry_changed, post_save
289
from django.utils import timezone
290
from myapp.models import TranslationMetrics
291
292
@receiver(entry_changed)
293
def track_translation_metrics(sender, **kwargs):
294
"""Track detailed translation activity metrics."""
295
296
user = kwargs['user']
297
old_msgstr = kwargs['old_msgstr']
298
language_code = kwargs['language_code']
299
300
# Determine action type
301
if not old_msgstr:
302
action_type = 'translation_added'
303
elif not kwargs.get('new_msgstr'): # Would need to get this from entry
304
action_type = 'translation_removed'
305
else:
306
action_type = 'translation_modified'
307
308
# Record metrics
309
TranslationMetrics.objects.create(
310
user=user,
311
language_code=language_code,
312
action_type=action_type,
313
timestamp=timezone.now()
314
)
315
316
@receiver(post_save)
317
def update_project_statistics(sender, **kwargs):
318
"""Update project-wide translation statistics."""
319
320
language_code = kwargs['language_code']
321
322
# Calculate updated statistics
323
from rosetta.poutil import find_pos
324
325
files = find_pos(language_code)
326
327
total_translated = 0
328
total_untranslated = 0
329
total_fuzzy = 0
330
331
for file_info in files:
332
stats = file_info['stats']
333
total_translated += stats.get('translated', 0)
334
total_untranslated += stats.get('untranslated', 0)
335
total_fuzzy += stats.get('fuzzy', 0)
336
337
# Store in cache for quick access
338
from django.core.cache import cache
339
cache.set(f'project_translation_stats_{language_code}', {
340
'translated': total_translated,
341
'untranslated': total_untranslated,
342
'fuzzy': total_fuzzy,
343
'completion_percentage': (total_translated / (total_translated + total_untranslated + total_fuzzy)) * 100 if (total_translated + total_untranslated + total_fuzzy) > 0 else 0,
344
'last_updated': timezone.now().isoformat()
345
}, timeout=3600) # Cache for 1 hour
346
```
347
348
### Signal Registration
349
350
```python
351
# myapp/apps.py
352
from django.apps import AppConfig
353
354
class MyAppConfig(AppConfig):
355
default_auto_field = 'django.db.models.BigAutoField'
356
name = 'myapp'
357
358
def ready(self):
359
# Import signal handlers to register them
360
import myapp.signals
361
362
# myapp/__init__.py
363
default_app_config = 'myapp.apps.MyAppConfig'
364
```
365
366
### Testing Signal Handlers
367
368
```python
369
# myapp/tests.py
370
from django.test import TestCase, RequestFactory
371
from django.contrib.auth.models import User
372
from rosetta.signals import entry_changed, post_save
373
from unittest.mock import Mock, patch
374
375
class SignalHandlerTests(TestCase):
376
def setUp(self):
377
self.factory = RequestFactory()
378
self.user = User.objects.create_user('testuser', 'test@example.com', 'pass')
379
380
def test_entry_changed_signal(self):
381
"""Test entry_changed signal is properly handled."""
382
383
request = self.factory.post('/')
384
request.user = self.user
385
386
# Mock POFile object
387
pofile = Mock()
388
pofile.fpath = '/path/to/test.po'
389
390
# Send signal
391
with patch('myapp.signals.logger') as mock_logger:
392
entry_changed.send(
393
sender=Mock,
394
user=self.user,
395
old_msgstr='old translation',
396
old_fuzzy=True,
397
pofile=pofile,
398
language_code='fr',
399
request=request
400
)
401
402
# Verify handler was called
403
mock_logger.info.assert_called()
404
405
def test_post_save_signal(self):
406
"""Test post_save signal is properly handled."""
407
408
request = self.factory.post('/')
409
request.user = self.user
410
411
# Send signal
412
with patch('myapp.signals.send_mail') as mock_send_mail:
413
post_save.send(
414
sender=Mock,
415
language_code='es',
416
request=request
417
)
418
419
# Verify notification was sent
420
mock_send_mail.assert_called_once()
421
```
422
423
### Signal Debugging
424
425
```python
426
# myapp/signals.py
427
import logging
428
from django.dispatch import receiver
429
from rosetta.signals import entry_changed, post_save
430
431
# Configure detailed logging for signal debugging
432
signal_logger = logging.getLogger('rosetta.signals')
433
434
@receiver(entry_changed)
435
def debug_entry_changed(sender, **kwargs):
436
"""Debug handler for entry_changed signal."""
437
438
signal_logger.debug(f"entry_changed signal received:")
439
signal_logger.debug(f" sender: {sender}")
440
signal_logger.debug(f" user: {kwargs.get('user')}")
441
signal_logger.debug(f" old_msgstr: {kwargs.get('old_msgstr')}")
442
signal_logger.debug(f" old_fuzzy: {kwargs.get('old_fuzzy')}")
443
signal_logger.debug(f" language_code: {kwargs.get('language_code')}")
444
signal_logger.debug(f" pofile: {kwargs.get('pofile')}")
445
446
@receiver(post_save)
447
def debug_post_save(sender, **kwargs):
448
"""Debug handler for post_save signal."""
449
450
signal_logger.debug(f"post_save signal received:")
451
signal_logger.debug(f" sender: {sender}")
452
signal_logger.debug(f" language_code: {kwargs.get('language_code')}")
453
signal_logger.debug(f" request.user: {kwargs.get('request', {}).user if kwargs.get('request') else 'None'}")
454
455
# In settings.py for debugging
456
LOGGING = {
457
'version': 1,
458
'disable_existing_loggers': False,
459
'handlers': {
460
'console': {
461
'class': 'logging.StreamHandler',
462
},
463
},
464
'loggers': {
465
'rosetta.signals': {
466
'handlers': ['console'],
467
'level': 'DEBUG',
468
'propagate': True,
469
},
470
},
471
}
472
```