diff --git a/apps/addons/models.py b/apps/addons/models.py index 927c2f3616..f7e4d530a7 100644 --- a/apps/addons/models.py +++ b/apps/addons/models.py @@ -41,8 +41,8 @@ from market.models import AddonPremium, Price from reviews.models import Review import sharing.utils as sharing from stats.models import AddonShareCountTotal -from translations.fields import (TranslatedField, PurifiedField, - LinkifiedField, Translation) +from translations.fields import (LinkifiedField, PurifiedField, save_signal, + TranslatedField, Translation) from translations.query import order_by_translation from users.models import UserProfile, UserForeignKey from users.utils import find_users @@ -1512,6 +1512,9 @@ class Addon(amo.models.OnChangeMixin, amo.models.ModelBase): # Not implemented for non-webapps. return '' +dbsignals.pre_save.connect(save_signal, sender=Addon, + dispatch_uid='addon_translations') + class AddonDeviceType(amo.models.ModelBase): addon = models.ForeignKey(Addon) @@ -1801,6 +1804,9 @@ class AddonType(amo.models.ModelBase): return None return reverse('browse.%s' % type) +dbsignals.pre_save.connect(save_signal, sender=AddonType, + dispatch_uid='addontype_translations') + class AddonUser(caching.CachingMixin, models.Model): addon = models.ForeignKey(Addon) @@ -1894,6 +1900,9 @@ class Category(amo.models.ModelBase): for addon in addons: addon.all_categories = cats.get(addon.id, []) +dbsignals.pre_save.connect(save_signal, sender=Category, + dispatch_uid='category_translations') + class CategorySupervisor(amo.models.ModelBase): category = models.ForeignKey(Category) @@ -2007,6 +2016,9 @@ class Preview(amo.models.ModelBase): def image_size(self): return self.sizes.get('image', []) if self.sizes else [] +dbsignals.pre_save.connect(save_signal, sender=Preview, + dispatch_uid='preview_translations') + class AppSupport(amo.models.ModelBase): """Cache to tell us if an add-on's current version supports an app.""" diff --git a/apps/bandwagon/models.py b/apps/bandwagon/models.py index 4180af602a..c284b8f736 100644 --- a/apps/bandwagon/models.py +++ b/apps/bandwagon/models.py @@ -22,7 +22,7 @@ from amo.urlresolvers import reverse from addons.models import Addon, AddonRecommendation from applications.models import Application from stats.models import CollectionShareCountTotal -from translations.fields import TranslatedField, LinkifiedField +from translations.fields import LinkifiedField, save_signal, TranslatedField from users.models import UserProfile from versions import compare @@ -376,10 +376,11 @@ class Collection(CollectionBase, amo.models.ModelBase): models.signals.post_save.connect(Collection.post_save, sender=Collection, dispatch_uid='coll.post_save') +models.signals.pre_save.connect(save_signal, sender=Collection, + dispatch_uid='coll_translations') models.signals.post_delete.connect(Collection.post_delete, sender=Collection, dispatch_uid='coll.post_delete') - class CollectionAddon(amo.models.ModelBase): addon = models.ForeignKey(Addon) collection = models.ForeignKey(Collection) @@ -404,6 +405,9 @@ class CollectionFeature(amo.models.ModelBase): class Meta(amo.models.ModelBase.Meta): db_table = 'collection_features' +models.signals.pre_save.connect(save_signal, sender=CollectionFeature, + dispatch_uid='collectionfeature_translations') + class CollectionPromo(amo.models.ModelBase): collection = models.ForeignKey(Collection, null=True) diff --git a/apps/devhub/models.py b/apps/devhub/models.py index a1854064ef..156f77cde4 100644 --- a/apps/devhub/models.py +++ b/apps/devhub/models.py @@ -23,7 +23,7 @@ from bandwagon.models import Collection from mkt.webapps.models import Webapp from reviews.models import Review from tags.models import Tag -from translations.fields import TranslatedField +from translations.fields import save_signal, TranslatedField from users.helpers import user_link from users.models import UserProfile from versions.models import Version @@ -74,6 +74,9 @@ class HubPromo(amo.models.ModelBase): def flush_urls(self): return ['*/developers*'] +models.signals.pre_save.connect(save_signal, sender=HubPromo, + dispatch_uid='hubpromo_translations') + class HubEvent(amo.models.ModelBase): name = models.CharField(max_length=255, default='') diff --git a/apps/editors/models.py b/apps/editors/models.py index 4a00487bde..9446892b43 100644 --- a/apps/editors/models.py +++ b/apps/editors/models.py @@ -17,7 +17,7 @@ from amo.urlresolvers import reverse from amo.utils import cache_ns_key, send_mail from addons.models import Addon from editors.sql_model import RawSQLModel -from translations.fields import TranslatedField +from translations.fields import save_signal, TranslatedField from users.models import UserProfile from versions.models import version_uploaded @@ -41,6 +41,9 @@ class CannedResponse(amo.models.ModelBase): def __unicode__(self): return unicode(self.name) +models.signals.pre_save.connect(save_signal, sender=CannedResponse, + dispatch_uid='cannedresponses_translations') + class AddonCannedResponseManager(amo.models.ManagerBase): def get_query_set(self): diff --git a/apps/localizers/models.py b/apps/localizers/models.py index cbc70d272e..115f7cbcf2 100644 --- a/apps/localizers/models.py +++ b/apps/localizers/models.py @@ -3,7 +3,7 @@ from django.db import models import caching.base import amo.models -from translations.fields import PurifiedField +from translations.fields import PurifiedField, save_signal class L10nEventlog(caching.base.CachingMixin, models.Model): @@ -35,3 +35,7 @@ class L10nSettings(amo.models.ModelBase): class Meta: db_table = 'l10n_settings' + + +models.signals.pre_save.connect(save_signal, sender=L10nSettings, + dispatch_uid='l10n_translations') diff --git a/apps/market/models.py b/apps/market/models.py index a0c8229c55..1bec6571a6 100644 --- a/apps/market/models.py +++ b/apps/market/models.py @@ -4,7 +4,7 @@ from django.db import connection, models from django.dispatch import receiver from django.utils import translation -from translations.fields import TranslatedField +from translations.fields import save_signal, TranslatedField import amo import amo.models @@ -110,6 +110,10 @@ class Price(amo.models.ModelBase): return [({'currency': o.currency, 'amount': o.price}) for c, o in self.currencies()] +models.signals.pre_save.connect(save_signal, sender=Price, + dispatch_uid='price_translations') + + class PriceCurrency(amo.models.ModelBase): currency = models.CharField(max_length=10, diff --git a/apps/reviews/models.py b/apps/reviews/models.py index 3219002e6e..93bc5abe8a 100644 --- a/apps/reviews/models.py +++ b/apps/reviews/models.py @@ -12,7 +12,7 @@ from tower import ugettext_lazy as _ import amo.models from amo.helpers import shared_url from amo.urlresolvers import reverse -from translations.fields import TranslatedField +from translations.fields import save_signal, TranslatedField from users.models import UserProfile log = logging.getLogger('z.review') @@ -115,8 +115,12 @@ class Review(amo.models.ModelBase): user_ids[user.id].user = user -models.signals.post_save.connect(Review.post_save, sender=Review) -models.signals.post_delete.connect(Review.post_delete, sender=Review) +models.signals.post_save.connect(Review.post_save, sender=Review, + dispatch_uid='review_post_save') +models.signals.post_delete.connect(Review.post_delete, sender=Review, + dispatch_uid='review_post_delete') +models.signals.pre_save.connect(save_signal, sender=Review, + dispatch_uid='review_translations') # TODO: translate old flags. diff --git a/apps/translations/fields.py b/apps/translations/fields.py index 2719f52fd5..d0bd52df25 100644 --- a/apps/translations/fields.py +++ b/apps/translations/fields.py @@ -5,6 +5,7 @@ from django.db.models.fields import related from django.utils import translation as translation_utils from django.utils.translation.trans_real import to_language +from .hold import add_translation, make_key, save_translations from .models import Translation, PurifiedTranslation, LinkifiedTranslation from .widgets import TransInput, TransTextarea @@ -155,7 +156,11 @@ class TranslationDescriptor(related.ReverseSingleRelatedObjectDescriptor): except AttributeError: # Create a brand new translation. translation = self.model.new(string, lang) - save_on_signal(instance, translation) + + # A new translation has been created and it might need to be saved. + # This adds the translation to the queue of translation that need + # to be saved for this instance. + add_translation(make_key(instance), translation) return translation def translation_from_dict(self, instance, lang, dict_): @@ -276,3 +281,13 @@ class LocaleList(dict): def zip(self): return zip(self.locales, self.seq) + + +def save_signal(sender, instance, **kw): + """ + Use this signal on a model to iterate through all the translations added + to the hold queue and save them all. Hook this up to the pre_save signal + on the model. + """ + if not kw.get('raw'): + save_translations(make_key(instance)) diff --git a/apps/translations/hold.py b/apps/translations/hold.py new file mode 100644 index 0000000000..2258abb851 --- /dev/null +++ b/apps/translations/hold.py @@ -0,0 +1,51 @@ +from threading import local + +from django.core.signals import request_finished + +_to_save = local() + + +def add_translation(key, translation): + """ + Queue a translation that needs to be saved for a particular object. To + generate the key, call make_key. + """ + if not hasattr(_to_save, 'translations'): + _to_save.translations = {} + + _to_save.translations.setdefault(key, []) + _to_save.translations[key].append(translation) + + +def clean_translations(sender, **kwargs): + """ + Removes all translations in the queue. + """ + if hasattr(_to_save, 'translations'): + _to_save.translations = {} + + +def make_key(obj): + """Returns a key for this object.""" + return id(obj) + + +def save_translations(key): + """ + For a given key, save all the translations. The key is used to ensure that + we only save the translations for the given object (and not all of them). + Once saved, they will be deleted. + """ + if not hasattr(_to_save, 'translations'): + return + + for trans in _to_save.translations.get(key, []): + is_new = trans.autoid is None + trans.save(force_insert=is_new, force_update=not is_new) + + if key in _to_save.translations: + del _to_save.translations[key] + + +# Ensure that on request completion, we flush out any unsaved translations. +request_finished.connect(clean_translations, dispatch_uid='clean_translations') diff --git a/apps/translations/tests/testapp/models.py b/apps/translations/tests/testapp/models.py index 2767df404f..6b1c8ec7d9 100644 --- a/apps/translations/tests/testapp/models.py +++ b/apps/translations/tests/testapp/models.py @@ -1,7 +1,8 @@ from django.db import models import amo.models -from translations.fields import TranslatedField, PurifiedField, LinkifiedField +from translations.fields import (LinkifiedField, PurifiedField, save_signal, + TranslatedField) class TranslatedModel(amo.models.ModelBase): @@ -10,6 +11,9 @@ class TranslatedModel(amo.models.ModelBase): default_locale = models.CharField(max_length=10) no_locale = TranslatedField(require_locale=False) +models.signals.pre_save.connect(save_signal, sender=TranslatedModel, + dispatch_uid='testapp_translatedmodel') + class UntranslatedModel(amo.models.ModelBase): """Make sure nothing is broken when a model doesn't have translations.""" @@ -20,3 +24,7 @@ class FancyModel(amo.models.ModelBase): """Mix it up with purified and linkified fields.""" purified = PurifiedField() linkified = LinkifiedField() + + +models.signals.pre_save.connect(save_signal, sender=FancyModel, + dispatch_uid='testapp_fancymodel') diff --git a/apps/users/models.py b/apps/users/models.py index 5ff23d77e4..f1c453c350 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -25,7 +25,7 @@ import amo import amo.models from access.models import Group, GroupUser from amo.urlresolvers import reverse -from translations.fields import PurifiedField +from translations.fields import save_signal, PurifiedField from translations.query import order_by_translation log = commonware.log.getLogger('z.users') @@ -443,6 +443,9 @@ class UserProfile(amo.models.OnChangeMixin, amo.models.ModelBase): """ return bool(getattr(self.get_preapproval(), 'paypal_key', '')) +models.signals.pre_save.connect(save_signal, sender=UserProfile, + dispatch_uid='userprofile_translations') + @dispatch.receiver(models.signals.post_save, sender=UserProfile, dispatch_uid='user.post_save') diff --git a/apps/versions/models.py b/apps/versions/models.py index edc83cdedc..b13ffaa981 100644 --- a/apps/versions/models.py +++ b/apps/versions/models.py @@ -22,8 +22,8 @@ from applications.models import Application, AppVersion from files import utils from files.models import File, Platform, cleanup_file from tower import ugettext as _ -from translations.fields import (TranslatedField, PurifiedField, - LinkifiedField) +from translations.fields import (LinkifiedField, PurifiedField, save_signal, + TranslatedField) from users.models import UserProfile from .compare import version_dict, version_int @@ -526,6 +526,8 @@ def clear_compatversion_cache_on_delete(sender, instance, **kw): version_uploaded = django.dispatch.Signal() +models.signals.pre_save.connect( + save_signal, sender=Version, dispatch_uid='version_translations') models.signals.post_save.connect( update_status, sender=Version, dispatch_uid='version_update_status') models.signals.post_save.connect( @@ -577,6 +579,9 @@ class License(amo.models.ModelBase): def __unicode__(self): return unicode(self.name) +models.signals.pre_save.connect( + save_signal, sender=License, dispatch_uid='version_translations') + class VersionComment(amo.models.ModelBase): """Editor comments for version discussion threads.""" diff --git a/mkt/webapps/models.py b/mkt/webapps/models.py index cfff8b220d..48b496ea57 100644 --- a/mkt/webapps/models.py +++ b/mkt/webapps/models.py @@ -34,6 +34,7 @@ from constants.applications import DEVICE_TYPES from files.models import File, nfd_str, Platform from files.utils import parse_addon, WebAppParser from lib.crypto import packaged +from translations.fields import save_signal from versions.models import Version from mkt.zadmin.models import FeaturedApp @@ -757,6 +758,8 @@ Webapp._meta.translated_fields = Addon._meta.translated_fields models.signals.post_save.connect(update_search_index, sender=Webapp, dispatch_uid='mkt.webapps.index') +models.signals.pre_save.connect(save_signal, sender=Webapp, + dispatch_uid='webapp_translations') @receiver(version_changed, dispatch_uid='update_cached_manifests')