def save_translation(entity, locale, string, plural_form=None, fuzzy=False): """Admin interface: save new or update existing translation in DB.""" approved = not fuzzy translations = Translation.objects.filter( entity=entity, locale=locale, plural_form=plural_form) translations_equal = translations.filter(string=string) translations_equal_count = translations_equal.count() # Save new translation if it doesn's exist yet if translations_equal_count == 0: unset_approved(translations) t = Translation( entity=entity, locale=locale, plural_form=plural_form, string=string, date=datetime.datetime.now(), approved=approved, fuzzy=fuzzy) t.save(stats=False) # Update existing translations if fuzzy status changes elif translations_equal_count > 0: t = translations_equal[0] if translations_equal_count > 1: try: t = translations_equal.get(approved=True) except Translation.DoesNotExist: t = translations_equal.latest("date") if t.fuzzy != fuzzy: unset_approved(translations) t.date = datetime.datetime.now() t.approved = approved t.fuzzy = fuzzy t.save(stats=False)
def translate(request, locale, slug, part): """Translate view.""" locale = get_object_or_404(Locale, code__iexact=locale) project = get_object_or_404(Project.objects.available(), slug=slug) if locale not in project.locales.all(): raise Http404 projects = ( Project.objects.available() .prefetch_related('subpage_set') .order_by('name') ) paths = [part] if part != 'all-resources' else None translations = Translation.for_locale_project_paths(locale, project, paths) return render(request, 'translate.html', { 'download_form': forms.DownloadFileForm(), 'upload_form': forms.UploadFileForm(), 'locale': locale, 'locales': Locale.objects.available(), 'part': part, 'project': project, 'projects': projects, 'authors': translations.authors().serialize(), 'counts_per_minute': translations.counts_per_minute(), })
def translate(request, locale, slug, part): """Translate view.""" locale = get_object_or_404(Locale, code__iexact=locale) project = get_object_or_404(Project.objects.available(), slug=slug) if locale not in project.locales.all(): raise Http404 projects = ( Project.objects.available() .prefetch_related('subpage_set') .order_by('name') ) paths = [part] if part != 'all-resources' else None authors = Translation.authors(locale, project, paths).serialize() return render(request, 'translate.html', { 'download_form': forms.DownloadFileForm(), 'upload_form': forms.UploadFileForm(), 'locale': locale, 'locales': Locale.objects.available(), 'part': part, 'project': project, 'projects': projects, 'authors': authors, })
def delete_translation(request): """Delete given translation.""" try: t = request.POST['translation'] paths = request.POST.getlist('paths[]') except MultiValueDictKeyError as e: return HttpResponseBadRequest('Bad Request: {error}'.format(error=e)) translation = get_object_or_404(Translation, pk=t) # Non-privileged users can only delete own unapproved translations if not request.user.has_perm('base.can_translate_locale', translation.locale): if translation.user == request.user: if translation.approved is True: return HttpResponseForbidden( "Forbidden: Can't delete approved translations" ) else: return HttpResponseForbidden( "Forbidden: Can't delete translations from other users" ) translation.delete() project = translation.entity.resource.project locale = translation.locale return JsonResponse({ 'stats': TranslatedResource.objects.stats(project, paths, locale), 'authors': Translation.authors(locale, project, paths).serialize(), })
def delete_translation(request): """Delete given translation.""" try: t = request.POST['translation'] paths = request.POST.getlist('paths[]') except MultiValueDictKeyError as e: return HttpResponseBadRequest('Bad Request: {error}'.format(error=e)) translation = get_object_or_404(Translation, pk=t) # Non-privileged users can only delete own unapproved translations if not request.user.can_translate(translation.locale, translation.entity.resource.project): if translation.user == request.user: if translation.approved is True: return HttpResponseForbidden( "Forbidden: Can't delete approved translations" ) else: return HttpResponseForbidden( "Forbidden: Can't delete translations from other users" ) translation.delete() project = translation.entity.resource.project locale = translation.locale translations = Translation.for_locale_project_paths(locale, project, paths) return JsonResponse({ 'stats': TranslatedResource.objects.stats(project, paths, locale), 'authors': translations.authors().serialize(), 'counts_per_minute': translations.counts_per_minute(), })
def execute_create_db(self): new_entities = [] for vcs_entity in self.changes['create_db']: # We can't use bulk_create since we need a PK entity, created = Entity.objects.get_or_create( **self.get_entity_updates(vcs_entity)) if created: new_entities.append(entity) for locale_code, vcs_translation in vcs_entity.translations.items( ): for plural_form, string in vcs_translation.strings.items(): self.translations_to_create.append( Translation(entity=entity, locale=self.locales[locale_code], string=string, plural_form=plural_form, approved=not vcs_translation.fuzzy, approved_date=self.now if not vcs_translation.fuzzy else None, fuzzy=vcs_translation.fuzzy)) self.send_notifications(new_entities)
def authors_and_time_range(request, locale, slug, part): locale = get_object_or_404(Locale, code=locale) project = get_object_or_404(Project.objects.available(), slug=slug) paths = [part] if part != 'all-resources' else None translations = Translation.for_locale_project_paths(locale, project, paths) return JsonResponse({ 'authors': translations.authors(), 'counts_per_minute': translations.counts_per_minute(), }, safe=False)
def authors_and_time_range(request, locale, slug, part): locale = get_object_or_404(Locale, code=locale) project = get_object_or_404(Project.objects.available(), slug=slug) paths = [part] if part != 'all-resources' else None translations = Translation.for_locale_project_paths(locale, project, paths) return JsonResponse({ 'authors': translations.authors().serialize(), 'counts_per_minute': translations.counts_per_minute(), }, safe=False)
def execute_update_db(self): for locale_code, db_entity, vcs_entity in self.changes['update_db']: for field, value in self.get_entity_updates(vcs_entity).items(): setattr(db_entity, field, value) if db_entity.is_dirty(check_relationship=True): self.entities_to_update.append(db_entity) # Update translations for the entity. vcs_translation = vcs_entity.translations[locale_code] db_translations = db_entity.translation_set.filter( locale__code=locale_code, ) approved_translations = [] for plural_form, string in vcs_translation.strings.items(): # Check if we need to modify an existing translation or # create a new one. db_translation = match_attr(db_translations, plural_form=plural_form, string=string) if db_translation: if not db_translation.approved: db_translation.approved = True db_translation.approved_date = self.now db_translation.fuzzy = vcs_translation.fuzzy db_translation.extra = vcs_translation.extra if db_translation.is_dirty(): self.translations_to_update.append(db_translation) if not db_translation.fuzzy: approved_translations.append(db_translation) else: self.translations_to_create.append( Translation(entity=db_entity, locale=self.locales[locale_code], string=string, plural_form=plural_form, approved=not vcs_translation.fuzzy, approved_date=self.now if not vcs_translation.fuzzy else None, fuzzy=vcs_translation.fuzzy, extra=vcs_translation.extra)) # Any existing translations that were not approved get unapproved. for translation in db_translations.filter( approved_date__lte=self.now): if translation not in approved_translations: translation.approved = False translation.approved_user = None translation.approved_date = None if translation.is_dirty(): self.translations_to_update.append(translation)
def _save_translation(entity, locale, string, plural_form=None, fuzzy=False): """Admin interface: save new or update existing translation in DB.""" approved = not fuzzy # Update existing translation if different from repository try: t = Translation.objects.get(entity=entity, locale=locale, plural_form=plural_form, approved=True) if t.string != string or t.fuzzy != fuzzy: t.string = string t.user = None t.date = datetime.datetime.now() t.approved = approved t.fuzzy = fuzzy t.save() # Save new translation if it doesn's exist yet except Translation.DoesNotExist: t = Translation( entity=entity, locale=locale, string=string, plural_form=plural_form, date=datetime.datetime.now(), approved=approved, fuzzy=fuzzy) t.save()
def execute_create_db(self): for vcs_entity in self.changes['create_db']: entity = Entity(**self.get_entity_updates(vcs_entity)) entity.save() # We can't use bulk_create since we need a PK for locale_code, vcs_translation in vcs_entity.translations.items(): for plural_form, string in vcs_translation.strings.items(): self.translations_to_create.append(Translation( entity=entity, locale=self.locales[locale_code], string=string, plural_form=plural_form, approved=not vcs_translation.fuzzy, approved_date=self.now if not vcs_translation.fuzzy else None, fuzzy=vcs_translation.fuzzy ))
def _save_translation(entity, locale, translation, author=""): """Admin interface: save new or update existing translation in DB.""" translations = Translation.objects.filter(entity=entity, locale=locale).order_by("date") if len(translations) == 0: # New translation t = Translation(entity=entity, locale=locale, string=translation, author=author, date=datetime.datetime.now()) else: # Update translation t = translations.reverse()[0] t.string = translation t.author = author t.date = datetime.datetime.now() t.save()
def update_translation(request): """Update entity translation for the specified locale and user. Note that this view is also used to approve a translation by the old Translate app. Once we migrate to Translate.Next, we'll want to rework this view to remove the bits about approving a translation, because that has been delegated to the `approve_translation` view. """ try: entity = request.POST["entity"] string = request.POST["translation"] locale = request.POST["locale"] plural_form = request.POST["plural_form"] original = request.POST["original"] ignore_warnings = request.POST.get("ignore_warnings", "false") == "true" approve = request.POST.get("approve", "false") == "true" force_suggestions = request.POST.get("force_suggestions", "false") == "true" paths = request.POST.getlist("paths[]") except MultiValueDictKeyError as e: return JsonResponse( { "status": False, "message": "Bad Request: {error}".format(error=e) }, status=400, ) try: e = Entity.objects.get(pk=entity) except Entity.DoesNotExist as e: return JsonResponse( { "status": False, "message": "Bad Request: {error}".format(error=e) }, status=400, ) try: locale = Locale.objects.get(code=locale) except Locale.DoesNotExist as e: return JsonResponse( { "status": False, "message": "Bad Request: {error}".format(error=e) }, status=400, ) if plural_form == "-1": plural_form = None user = request.user project = e.resource.project # Read-only translations cannot saved if utils.readonly_exists(project, locale): return JsonResponse( { "status": False, "message": "Forbidden: This string is in read-only mode.", }, status=403, ) now = timezone.now() can_translate = request.user.can_translate( project=project, locale=locale) and (not force_suggestions or approve) translations = Translation.objects.filter(entity=e, locale=locale, plural_form=plural_form) same_translations = translations.filter(string=string).order_by( "-active", "rejected", "-date") # If same translation exists in the DB, don't save it again. if utils.is_same(same_translations, can_translate): return JsonResponse({"same": True}) # Check for errors. # Checks are disabled for the tutorial. use_checks = project.slug != "tutorial" failed_checks = None if use_checks: failed_checks = run_checks( e, locale.code, original, string, user.profile.quality_checks, ) if are_blocking_checks(failed_checks, ignore_warnings): return JsonResponse({"failedChecks": failed_checks}) # Translations exist if len(translations) > 0: # Same translation exists if len(same_translations) > 0: t = same_translations[0] # If added by privileged user, approve and unfuzzy it if can_translate and (t.fuzzy or not t.approved): if not t.active: translations.filter(active=True).update(active=False) t.active = True t.approved = True t.fuzzy = False t.rejected = False t.rejected_user = None t.rejected_date = None if t.approved_user is None: t.approved_user = user t.approved_date = now t.save(failed_checks=failed_checks) log_action("translation:approved", user, translation=t) return JsonResponse({ "type": "updated", "translation": t.serialize(), "stats": TranslatedResource.objects.stats(project, paths, locale), }) # Different translation added else: t = Translation( entity=e, locale=locale, plural_form=plural_form, string=string, user=user, date=now, approved=can_translate, ) if can_translate: t.approved_user = user t.approved_date = now t.save(failed_checks=failed_checks) log_action("translation:created", user, translation=t) active_translation = e.reset_active_translation( locale=locale, plural_form=plural_form, ) return JsonResponse({ "type": "added", "translation": active_translation.serialize(), "stats": TranslatedResource.objects.stats(project, paths, locale), }) # No translations saved yet else: t = Translation( entity=e, locale=locale, plural_form=plural_form, string=string, user=user, date=now, active=True, approved=can_translate, ) if can_translate: t.approved_user = user t.approved_date = now t.save(failed_checks=failed_checks) log_action("translation:created", user, translation=t) return JsonResponse({ "type": "saved", "translation": t.serialize(), "stats": TranslatedResource.objects.stats(project, paths, locale), })
def update_translation(request): """Update entity translation for the specified locale and user.""" try: entity = request.POST["entity"] string = request.POST["translation"] locale = request.POST["locale"] plural_form = request.POST["plural_form"] original = request.POST["original"] ignore_check = request.POST["ignore_check"] approve = json.loads(request.POST["approve"]) paths = request.POST.getlist("paths[]") except MultiValueDictKeyError as e: return HttpResponseBadRequest("Bad Request: {error}".format(error=e)) try: e = Entity.objects.get(pk=entity) except Entity.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") try: l = Locale.objects.get(code__iexact=locale) except Locale.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") if plural_form == "-1": plural_form = None user = request.user if not request.user.is_authenticated(): if e.resource.project.pk != 1: log.error("Not authenticated") return HttpResponse("error") else: user = None try: quality_checks = UserProfile.objects.get(user=user).quality_checks except UserProfile.DoesNotExist as error: quality_checks = True ignore = False if ignore_check == "true" or not quality_checks: ignore = True now = timezone.now() can_translate = request.user.has_perm("base.can_translate_locale", l) and ( not request.user.profile.force_suggestions or approve ) translations = Translation.objects.filter(entity=e, locale=l, plural_form=plural_form) # Newlines are not allowed in .lang files (bug 1190754) if e.resource.format == "lang" and "\n" in string: return HttpResponse("Newline characters are not allowed.") # Translations exist if len(translations) > 0: # Same translation exists try: t = translations.get(string=string) # If added by privileged user, approve and unfuzzy it if can_translate: # Unless there's nothing to be changed if t.user is not None and t.approved and t.approved_user and t.approved_date and not t.fuzzy: return HttpResponse("Same translation already exists.") warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings translations.update(approved=False, approved_user=None, approved_date=None) translations.update(fuzzy=False) if t.user is None: t.user = user t.approved = True t.approved_date = timezone.now() t.fuzzy = False if t.approved_user is None: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() return JsonResponse( { "type": "updated", "translation": t.serialize(), "stats": TranslatedResource.objects.stats(e.resource.project, paths, l), } ) # If added by non-privileged user, unfuzzy it else: if t.fuzzy: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings if t.user is None: t.user = user t.approved = False t.approved_user = None t.approved_date = None t.fuzzy = False if request.user.is_authenticated(): t.save() return JsonResponse( { "type": "updated", "translation": t.serialize(), "stats": TranslatedResource.objects.stats(e.resource.project, paths, l), } ) return HttpResponse("Same translation already exists.") # Different translation added except: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings if can_translate: translations.update(approved=False, approved_user=None, approved_date=None) translations.update(fuzzy=False) t = Translation( entity=e, locale=l, user=user, string=string, plural_form=plural_form, date=now, approved=can_translate ) if can_translate: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() # Return active (approved or latest) translation try: active = translations.filter(approved=True).latest("date") except Translation.DoesNotExist: active = translations.latest("date") return JsonResponse( { "type": "added", "translation": active.serialize(), "stats": TranslatedResource.objects.stats(e.resource.project, paths, l), } ) # No translations saved yet else: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings t = Translation( entity=e, locale=l, user=user, string=string, plural_form=plural_form, date=now, approved=can_translate ) if can_translate: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() return JsonResponse( { "type": "saved", "translation": t.serialize(), "stats": TranslatedResource.objects.stats(e.resource.project, paths, l), } )
def test_manage_project_strings_download_csv(self): locale_kl = LocaleFactory.create(code='kl', name='Klingon') locale_gs = LocaleFactory.create(code='gs', name='Geonosian') project = ProjectFactory.create(data_source='database', locales=[locale_kl, locale_gs], repositories=[]) url = reverse('pontoon.admin.project.strings', args=(project.slug, )) new_strings = """ And on the pedestal these words appear: 'My name is Ozymandias, king of kings: Look on my works, ye Mighty, and despair!' """ response = self.client.post(url, {'new_strings': new_strings}) assert_code(response, 200) # Test downloading the data. response = self.client.get(url, {'format': 'csv'}) assert_code(response, 200) assert_equal(response._headers['content-type'], ('Content-Type', 'text/csv')) # Verify the original content is here. assert_contains(response, 'pedestal') assert_contains(response, 'Ozymandias') assert_contains(response, 'Mighty') # Verify we have the locale columns. assert_contains(response, 'kl') assert_contains(response, 'gs') # Now add some translations. entity = Entity.objects.filter( string='And on the pedestal these words appear:')[0] Translation( string='Et sur le piédestal il y a ces mots :', entity=entity, locale=locale_kl, approved=True, ).save() Translation( string='Und auf dem Sockel steht die Schrift: ‚Mein Name', entity=entity, locale=locale_gs, approved=True, ).save() entity = Entity.objects.filter( string='\'My name is Ozymandias, king of kings:')[0] Translation( string='"Mon nom est Ozymandias, Roi des Rois.', entity=entity, locale=locale_kl, approved=True, ).save() Translation( string='Ist Osymandias, aller Kön’ge König: –', entity=entity, locale=locale_gs, approved=True, ).save() entity = Entity.objects.filter( string='Look on my works, ye Mighty, and despair!\'')[0] Translation( string='Voyez mon œuvre, vous puissants, et désespérez !"', entity=entity, locale=locale_kl, approved=True, ).save() Translation( string='Seht meine Werke, Mächt’ge, und erbebt!‘', entity=entity, locale=locale_gs, approved=True, ).save() response = self.client.get(url, {'format': 'csv'}) # Verify the translated content is here. assert_contains(response, 'pedestal') assert_contains(response, 'piédestal') assert_contains(response, 'Sockel') assert_contains(response, 'Mighty') assert_contains(response, 'puissants') assert_contains(response, 'Mächt’ge')
def create_translation(request): """ Create a new translation. """ form = forms.CreateTranslationForm(request.POST) if not form.is_valid(): problems = [] for field, errors in form.errors.items(): problems.append('Error validating field `{0}`: "{1}"'.format( field, " ".join(errors))) return JsonResponse({ "status": False, "message": "\n".join(problems) }, status=400) entity = form.cleaned_data["entity"] string = form.cleaned_data["translation"] locale = form.cleaned_data["locale"] plural_form = form.cleaned_data["plural_form"] original = form.cleaned_data["original"] ignore_warnings = form.cleaned_data["ignore_warnings"] approve = form.cleaned_data["approve"] force_suggestions = form.cleaned_data["force_suggestions"] resources = form.cleaned_data["paths"] project = entity.resource.project # Read-only translations cannot saved if utils.readonly_exists(project, locale): return JsonResponse( { "status": False, "message": "Forbidden: This string is in read-only mode.", }, status=403, ) translations = Translation.objects.filter(entity=entity, locale=locale, plural_form=plural_form) same_translations = translations.filter(string=string) # If same translation exists in the DB, don't save it again. if same_translations: return JsonResponse({"status": False, "same": True}) # Look for failed checks. # Checks are disabled for the tutorial. use_checks = project.slug != "tutorial" user = request.user failed_checks = None if use_checks: failed_checks = run_checks( entity, locale.code, original, string, user.profile.quality_checks, ) if are_blocking_checks(failed_checks, ignore_warnings): return JsonResponse({ "status": False, "failedChecks": failed_checks }) now = timezone.now() can_translate = user.can_translate( project=project, locale=locale) and (not force_suggestions or approve) translation = Translation( entity=entity, locale=locale, plural_form=plural_form, string=string, user=user, date=now, approved=can_translate, ) if can_translate: translation.approved_user = user translation.approved_date = now translation.save(failed_checks=failed_checks) log_action("translation:created", user, translation=translation) if translations: translation = entity.reset_active_translation( locale=locale, plural_form=plural_form, ) return JsonResponse({ "status": True, "translation": translation.serialize(), "stats": TranslatedResource.objects.stats(project, resources, locale), })
def update_translation(request): """Update entity translation for the specified locale and user.""" try: entity = request.POST['entity'] string = request.POST['translation'] locale = request.POST['locale'] plural_form = request.POST['plural_form'] original = request.POST['original'] ignore_check = request.POST['ignore_check'] except MultiValueDictKeyError as error: log.error(str(error)) return HttpResponse("error") try: e = Entity.objects.get(pk=entity) except Entity.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") try: l = Locale.objects.get(code__iexact=locale) except Locale.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") if plural_form == "-1": plural_form = None user = request.user if not request.user.is_authenticated(): if e.resource.project.pk != 1: log.error("Not authenticated") return HttpResponse("error") else: user = None try: quality_checks = UserProfile.objects.get(user=user).quality_checks except UserProfile.DoesNotExist as error: quality_checks = True ignore = False if ignore_check == 'true' or not quality_checks: ignore = True now = timezone.now() can_translate = ( request.user.has_perm('base.can_translate_locale', l) and not request.user.profile.force_suggestions ) translations = Translation.objects.filter( entity=e, locale=l, plural_form=plural_form) # Newlines are not allowed in .lang files (bug 1190754) if e.resource.format == 'lang' and '\n' in string: return HttpResponse('Newline characters are not allowed.') # Translations exist if len(translations) > 0: # Same translation exists try: t = translations.get(string=string) # If added by privileged user, approve and unfuzzy it if can_translate: # Unless there's nothing to be changed if t.user is not None and t.approved and t.approved_user \ and t.approved_date and not t.fuzzy: return HttpResponse("Same translation already exists.") warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings unapprove(translations) unfuzzy(translations) if t.user is None: t.user = user t.approved = True t.approved_date = timezone.now() t.fuzzy = False if t.approved_user is None: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() return HttpResponse(json.dumps({ 'type': 'updated', 'translation': t.serialize(), }), content_type='application/json') # If added by non-privileged user, unfuzzy it else: if t.fuzzy: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings if t.user is None: t.user = user t.approved = False t.approved_user = None t.approved_date = None t.fuzzy = False if request.user.is_authenticated(): t.save() return HttpResponse(json.dumps({ 'type': 'updated', 'translation': t.serialize(), }), content_type='application/json') return HttpResponse("Same translation already exists.") # Different translation added except: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings if can_translate: unapprove(translations) unfuzzy(translations) t = Translation( entity=e, locale=l, user=user, string=string, plural_form=plural_form, date=now, approved=can_translate) if can_translate: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() active = get_translation( entity=e, locale=l, plural_form=plural_form) return HttpResponse(json.dumps({ 'type': 'added', 'translation': active.serialize(), }), content_type='application/json') # No translations saved yet else: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings t = Translation( entity=e, locale=l, user=user, string=string, plural_form=plural_form, date=now, approved=can_translate) if can_translate: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() return HttpResponse(json.dumps({ 'type': 'saved', 'translation': t.serialize(), }), content_type='application/json')
def update_translation(request): """Update entity translation for the specified locale and user.""" try: entity = request.POST['entity'] string = request.POST['translation'] locale = request.POST['locale'] plural_form = request.POST['plural_form'] original = request.POST['original'] ignore_warnings = request.POST.get('ignore_warnings', 'false') == 'true' approve = request.POST.get('approve', 'false') == 'true' force_suggestions = request.POST.get('force_suggestions', 'false') == 'true' paths = request.POST.getlist('paths[]') except MultiValueDictKeyError as e: return HttpResponseBadRequest('Bad Request: {error}'.format(error=e)) try: e = Entity.objects.get(pk=entity) except Entity.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") try: locale = Locale.objects.get(code=locale) except Locale.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") if plural_form == "-1": plural_form = None user = request.user project = e.resource.project # Read-only translations cannot saved if utils.readonly_exists(project, locale): return HttpResponseForbidden( "Forbidden: This string is in read-only mode" ) try: use_ttk_checks = UserProfile.objects.get(user=user).quality_checks except UserProfile.DoesNotExist as error: use_ttk_checks = True # Disable checks for tutorial project. if project.slug == 'tutorial': use_ttk_checks = False now = timezone.now() can_translate = ( request.user.can_translate(project=project, locale=locale) and (not force_suggestions or approve) ) translations = Translation.objects.filter( entity=e, locale=locale, plural_form=plural_form) same_translations = translations.filter(string=string).order_by( '-approved', 'rejected', '-date' ) # If same translation exists in the DB, don't save it again. if utils.is_same(same_translations, can_translate): return JsonResponse({ 'same': True, }) failed_checks = run_checks( e, locale.code, original, string, use_ttk_checks, ) if are_blocking_checks(failed_checks, ignore_warnings): return JsonResponse({ 'failedChecks': failed_checks, }) # Translations exist if len(translations) > 0: if len(same_translations) > 0: t = same_translations[0] # If added by privileged user, approve and unfuzzy it if can_translate and (t.fuzzy or not t.approved): translations.update( approved=False, approved_user=None, approved_date=None, rejected=True, rejected_user=request.user, rejected_date=timezone.now(), fuzzy=False, ) t.approved = True t.fuzzy = False t.rejected = False t.rejected_user = None t.rejected_date = None if t.approved_user is None: t.approved_user = user t.approved_date = now # If added by non-privileged user and fuzzy, unfuzzy it elif t.fuzzy: t.approved = False t.approved_user = None t.approved_date = None t.fuzzy = False t.save() t.warnings.all().delete() t.errors.all().delete() save_failed_checks(t, failed_checks) return JsonResponse({ 'type': 'updated', 'translation': t.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, locale), }) # Different translation added else: if can_translate: translations.update(approved=False, approved_user=None, approved_date=None) translations.update(fuzzy=False) t = Translation( entity=e, locale=locale, user=user, string=string, plural_form=plural_form, date=now, approved=can_translate) if can_translate: t.approved_user = user t.approved_date = now t.save() save_failed_checks(t, failed_checks) # Return active (approved or latest) translation try: active = translations.filter(approved=True).latest("date") except Translation.DoesNotExist: active = translations.latest("date") return JsonResponse({ 'type': 'added', 'translation': active.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, locale), }) # No translations saved yet else: t = Translation( entity=e, locale=locale, user=user, string=string, plural_form=plural_form, date=now, approved=can_translate) if can_translate: t.approved_user = user t.approved_date = now t.save() save_failed_checks(t, failed_checks) return JsonResponse({ 'type': 'saved', 'translation': t.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, locale), })
def update_entity_translations_from_vcs( self, db_entity, locale_code, vcs_translation, user=None, db_translations=None, db_translations_approved_before_sync=None): if db_translations is None: db_translations = db_entity.translation_set.filter( locale__code=locale_code, ) if db_translations_approved_before_sync is None: db_translations_approved_before_sync = db_translations.filter( approved_date__lte=self.now) approved_translations = [] fuzzy_translations = [] for plural_form, string in vcs_translation.strings.items(): db_translation = match_attr(db_translations, plural_form=plural_form, string=string) # Modify existing translation. if db_translation: if not db_translation.approved and not vcs_translation.fuzzy: db_translation.approved = True db_translation.approved_user = user db_translation.approved_date = self.now db_translation.rejected = False db_translation.fuzzy = vcs_translation.fuzzy db_translation.extra = vcs_translation.extra if db_translation.is_dirty(): self.translations_to_update[ db_translation.pk] = db_translation if db_translation.fuzzy: fuzzy_translations.append(db_translation) else: approved_translations.append(db_translation) # Create new translation. else: self.translations_to_create.append( Translation(entity=db_entity, locale=self.locales[locale_code], string=string, plural_form=plural_form, approved=not vcs_translation.fuzzy, approved_user=user, approved_date=self.now if not vcs_translation.fuzzy else None, user=user, fuzzy=vcs_translation.fuzzy, extra=vcs_translation.extra)) # Unapprove translations that were approved before the sync job started unless sync # resolves them as active approved translations. # Note: If translations get approved after the sync starts, duplicate approvals can arise. # We take care of that at the and of the sync job in tasks.py. for translation in db_translations_approved_before_sync: if translation not in approved_translations: # Use the translation instance already set for update if it exists. translation = self.translations_to_update.get( translation.pk, translation) translation.approved = False translation.approved_user = None translation.approved_date = None # Reject translations unless they became fuzzy during sync. Condition is sufficient # because they were approved previously. if not translation.fuzzy: translation.rejected = True translation.rejected_user = user translation.rejected_date = self.now if translation.is_dirty(): self.translations_to_update[translation.pk] = translation # Unfuzzy existing translations unless sync resolves them as active fuzzy translations. # Note: Translations cannot get fuzzy after the sync job starts, because they cannot be # made fuzzy in Pontoon. for translation in db_translations: if translation not in fuzzy_translations: # Use the translation instance already set for update if it exists. translation = self.translations_to_update.get( translation.pk, translation) translation.fuzzy = False if translation.is_dirty(): self.translations_to_update[translation.pk] = translation
def update_translation(request): """Update entity translation for the specified locale and user.""" try: entity = request.POST['entity'] string = request.POST['translation'] locale = request.POST['locale'] plural_form = request.POST['plural_form'] original = request.POST['original'] ignore_check = request.POST['ignore_check'] approve = request.POST.get('approve', 'false') == 'true' force_suggestions = request.POST.get('force_suggestions', 'false') == 'true' paths = request.POST.getlist('paths[]') except MultiValueDictKeyError as e: return HttpResponseBadRequest('Bad Request: {error}'.format(error=e)) try: e = Entity.objects.get(pk=entity) except Entity.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") try: l = Locale.objects.get(code=locale) except Locale.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") if plural_form == "-1": plural_form = None user = request.user project = e.resource.project try: quality_checks = UserProfile.objects.get(user=user).quality_checks except UserProfile.DoesNotExist as error: quality_checks = True ignore = False if ignore_check == 'true' or not quality_checks: ignore = True now = timezone.now() can_translate = ( request.user.can_translate(project=project, locale=l) and (not force_suggestions or approve) ) translations = Translation.objects.filter( entity=e, locale=l, plural_form=plural_form) # Newlines are not allowed in .lang files (bug 1190754) if e.resource.format == 'lang' and '\n' in string: return HttpResponse('Newline characters are not allowed.') # Translations exist if len(translations) > 0: # Same translation exists same_translations = translations.filter(string=string).order_by('-approved', '-date') if len(same_translations) > 0: t = same_translations[0] # If added by privileged user, approve and unfuzzy it if can_translate: # Unless there's nothing to be changed if t.user is not None and t.approved and t.approved_user \ and t.approved_date and not t.fuzzy: return JsonResponse({ 'same': True, 'message': 'Same translation already exists.', }) warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings translations.update(approved=False, approved_user=None, approved_date=None) translations.update(fuzzy=False) if t.user is None: t.user = user t.approved = True t.approved_date = timezone.now() t.fuzzy = False if t.approved_user is None: t.approved_user = user t.approved_date = now t.save() return JsonResponse({ 'type': 'updated', 'translation': t.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, l), }) # If added by non-privileged user, unfuzzy it else: if t.fuzzy: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings if t.user is None: t.user = user t.approved = False t.approved_user = None t.approved_date = None t.fuzzy = False t.save() return JsonResponse({ 'type': 'updated', 'translation': t.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, l), }) return JsonResponse({ 'same': True, 'message': 'Same translation already exists.', }) # Different translation added else: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings if can_translate: translations.update(approved=False, approved_user=None, approved_date=None) translations.update(fuzzy=False) t = Translation( entity=e, locale=l, user=user, string=string, plural_form=plural_form, date=now, approved=can_translate) if can_translate: t.approved_user = user t.approved_date = now t.save() # Return active (approved or latest) translation try: active = translations.filter(approved=True).latest("date") except Translation.DoesNotExist: active = translations.latest("date") return JsonResponse({ 'type': 'added', 'translation': active.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, l), }) # No translations saved yet else: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings t = Translation( entity=e, locale=l, user=user, string=string, plural_form=plural_form, date=now, approved=can_translate) if can_translate: t.approved_user = user t.approved_date = now t.save() return JsonResponse({ 'type': 'saved', 'translation': t.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, l), })
def update_translation(request): """Update entity translation for the specified locale and user. Note that this view is also used to approve a translation by the old Translate app. Once we migrate to Translate.Next, we'll want to rework this view to remove the bits about approving a translation, because that has been delegated to the `approve_translation` view. """ try: entity = request.POST['entity'] string = request.POST['translation'] locale = request.POST['locale'] plural_form = request.POST['plural_form'] original = request.POST['original'] ignore_warnings = request.POST.get('ignore_warnings', 'false') == 'true' approve = request.POST.get('approve', 'false') == 'true' force_suggestions = request.POST.get('force_suggestions', 'false') == 'true' paths = request.POST.getlist('paths[]') except MultiValueDictKeyError as e: return HttpResponseBadRequest('Bad Request: {error}'.format(error=e)) try: e = Entity.objects.get(pk=entity) except Entity.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") try: locale = Locale.objects.get(code=locale) except Locale.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") if plural_form == "-1": plural_form = None user = request.user project = e.resource.project # Read-only translations cannot saved if utils.readonly_exists(project, locale): return HttpResponseForbidden( "Forbidden: This string is in read-only mode" ) try: use_ttk_checks = UserProfile.objects.get(user=user).quality_checks except UserProfile.DoesNotExist as error: use_ttk_checks = True # Disable checks for tutorial project. if project.slug == 'tutorial': use_ttk_checks = False now = timezone.now() can_translate = ( request.user.can_translate(project=project, locale=locale) and (not force_suggestions or approve) ) translations = Translation.objects.filter( entity=e, locale=locale, plural_form=plural_form) same_translations = translations.filter(string=string).order_by( '-active', 'rejected', '-date' ) # If same translation exists in the DB, don't save it again. if utils.is_same(same_translations, can_translate): return JsonResponse({ 'same': True, }) failed_checks = run_checks( e, locale.code, original, string, use_ttk_checks, ) if are_blocking_checks(failed_checks, ignore_warnings): return JsonResponse({ 'failedChecks': failed_checks, }) # Translations exist if len(translations) > 0: # Same translation exists if len(same_translations) > 0: t = same_translations[0] # If added by privileged user, approve and unfuzzy it if can_translate and (t.fuzzy or not t.approved): if not t.active: translations.filter(active=True).update(active=False) t.active = True t.approved = True t.fuzzy = False t.rejected = False t.rejected_user = None t.rejected_date = None if t.approved_user is None: t.approved_user = user t.approved_date = now t.save() t.warnings.all().delete() t.errors.all().delete() save_failed_checks(t, failed_checks) return JsonResponse({ 'type': 'updated', 'translation': t.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, locale), }) # Different translation added else: t = Translation( entity=e, locale=locale, plural_form=plural_form, string=string, user=user, date=now, approved=can_translate, ) if can_translate: t.approved_user = user t.approved_date = now t.save() save_failed_checks(t, failed_checks) active_translation = e.reset_active_translation( locale=locale, plural_form=plural_form, ) return JsonResponse({ 'type': 'added', 'translation': active_translation.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, locale), }) # No translations saved yet else: t = Translation( entity=e, locale=locale, plural_form=plural_form, string=string, user=user, date=now, active=True, approved=can_translate, ) if can_translate: t.approved_user = user t.approved_date = now t.save() save_failed_checks(t, failed_checks) return JsonResponse({ 'type': 'saved', 'translation': t.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, locale), })
def update_entity_translations_from_vcs(self, db_entity, locale_code, vcs_translation, user=None, db_translations=None, old_translations=None): if db_translations is None: db_translations = db_entity.translation_set.filter( locale__code=locale_code, ) approved_translations = [] fuzzy_translations = [] for plural_form, string in vcs_translation.strings.items(): # Check if we need to modify an existing translation or # create a new one. db_translation = match_attr(db_translations, plural_form=plural_form, string=string) if db_translation: if not db_translation.approved and not vcs_translation.fuzzy: db_translation.approved = True db_translation.approved_date = self.now db_translation.approved_user = user db_translation.fuzzy = vcs_translation.fuzzy db_translation.extra = vcs_translation.extra if db_translation.is_dirty(): self.translations_to_update.append(db_translation) if not db_translation.fuzzy: approved_translations.append(db_translation) else: fuzzy_translations.append(db_translation) else: self.translations_to_create.append( Translation(entity=db_entity, locale=self.locales[locale_code], string=string, plural_form=plural_form, approved=not vcs_translation.fuzzy, approved_date=self.now if not vcs_translation.fuzzy else None, approved_user=user, user=user, fuzzy=vcs_translation.fuzzy, extra=vcs_translation.extra)) # Any existing translations that were not approved get unapproved. if old_translations is None: old_translations = db_translations.filter( approved_date__lte=self.now) for translation in old_translations: if translation not in approved_translations: translation.approved = False translation.approved_user = None translation.approved_date = None if translation.is_dirty(): self.translations_to_update.append(translation) # Any existing translations that are no longer fuzzy get unfuzzied. for translation in db_translations: if translation not in fuzzy_translations: translation.fuzzy = False if translation.is_dirty(): self.translations_to_update.append(translation)
def update_translation(request, template=None): """Update entity translation for the specified locale and user.""" log.debug("Update entity translation for the specified locale and user.") if not request.is_ajax(): log.error("Non-AJAX request") raise Http404 if request.method != 'POST': log.error("Non-POST request") raise Http404 try: entity = request.POST['entity'] string = request.POST['translation'] locale = request.POST['locale'] plural_form = request.POST['plural_form'] original = request.POST['original'] ignore_check = request.POST['ignore_check'] except MultiValueDictKeyError as error: log.error(str(error)) return HttpResponse("error") log.debug("Entity: " + entity) log.debug("Translation: " + string) log.debug("Locale: " + locale) try: e = Entity.objects.get(pk=entity) except Entity.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") try: l = Locale.objects.get(code__iexact=locale) except Locale.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") if plural_form == "-1": plural_form = None user = request.user if not request.user.is_authenticated(): if e.resource.project.pk != 1: log.error("Not authenticated") return HttpResponse("error") else: user = None try: quality_checks = UserProfile.objects.get(user=user).quality_checks except UserProfile.DoesNotExist as error: quality_checks = True ignore = False if ignore_check == 'true' or not quality_checks: ignore = True now = timezone.now() can_localize = request.user.has_perm('base.can_localize') translations = Translation.objects.filter(entity=e, locale=l, plural_form=plural_form) # Newlines are not allowed in .lang files (bug 1190754) if e.resource.format == 'lang' and '\n' in string: return HttpResponse('Newline characters are not allowed.') # Translations exist if len(translations) > 0: # Same translation exists try: t = translations.get(string=string) # If added by privileged user, approve and unfuzzy it if can_localize: # Unless there's nothing to be changed if t.user is not None and t.approved and t.approved_user \ and t.approved_date and not t.fuzzy: return HttpResponse("Same translation already exists.") warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings unapprove(translations) unfuzzy(translations) if t.user is None: t.user = user t.approved = True t.approved_date = timezone.now() t.fuzzy = False if t.approved_user is None: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() return HttpResponse(json.dumps({ 'type': 'updated', 'translation': t.serialize(), }), content_type='application/json') # If added by non-privileged user, unfuzzy it else: if t.fuzzy: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings if t.user is None: t.user = user t.approved = False t.approved_user = None t.approved_date = None t.fuzzy = False if request.user.is_authenticated(): t.save() return HttpResponse(json.dumps({ 'type': 'updated', 'translation': t.serialize(), }), content_type='application/json') return HttpResponse("Same translation already exists.") # Different translation added except: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings if can_localize: unapprove(translations) unfuzzy(translations) t = Translation(entity=e, locale=l, user=user, string=string, plural_form=plural_form, date=now, approved=can_localize) if can_localize: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() active = get_translation(entity=e, locale=l, plural_form=plural_form) return HttpResponse(json.dumps({ 'type': 'added', 'translation': active.serialize(), }), content_type='application/json') # No translations saved yet else: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings t = Translation(entity=e, locale=l, user=user, string=string, plural_form=plural_form, date=now, approved=can_localize) if can_localize: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() return HttpResponse(json.dumps({ 'type': 'saved', 'translation': t.serialize(), }), content_type='application/json')
def entities(request): """Get entities for the specified project, locale and paths.""" try: project = request.POST['project'] locale = request.POST['locale'] paths = request.POST.getlist('paths[]') limit = int(request.POST.get('limit', 50)) except (MultiValueDictKeyError, ValueError) as err: return HttpResponseBadRequest('Bad Request: {error}'.format(error=err)) project = get_object_or_404(Project, slug=project) locale = get_object_or_404(Locale, code__iexact=locale) status = request.POST.get('status', '') extra = request.POST.get('extra', '') time = request.POST.get('time', '') author = request.POST.get('author', '') search = request.POST.get('search', '') exclude_entities = split_ints(request.POST.get('excludeEntities', '')) # Only return entities with provided IDs (batch editing) entity_ids = split_ints(request.POST.get('entityIds', '')) if entity_ids: entities = ( Entity.objects.filter(pk__in=entity_ids) .prefetch_resources_translations(locale) .distinct() .order_by('order') ) translations = Translation.for_locale_project_paths(locale, project, paths) return JsonResponse({ 'entities': Entity.map_entities(locale, entities), 'stats': TranslatedResource.objects.stats(project, paths, locale), 'authors': translations.authors().serialize(), 'counts_per_minute': translations.counts_per_minute(), }, safe=False) entities = Entity.for_project_locale( project, locale, paths, status, search, exclude_entities, extra, time, author ) # Only return a list of entity PKs (batch editing: select all) if request.POST.get('pkOnly', None): return JsonResponse({ 'entity_pks': list(entities.values_list('pk', flat=True)), }) visible_entities = [] # In-place view: load all entities if request.POST.get('inplaceEditor', None): has_next = False entities_to_map = Entity.for_project_locale( project, locale, paths, None, None, exclude_entities ) visible_entities = entities.values_list('pk', flat=True) # Out-of-context view: paginate entities else: paginator = Paginator(entities, limit) try: entities_page = paginator.page(1) except EmptyPage: return JsonResponse({ 'has_next': False, 'stats': {}, 'authors': [], 'counts_per_minute': [], }) has_next = entities_page.has_next() entities_to_map = entities_page.object_list # If requested entity not on the first page entity = request.POST.get('entity', None) if entity: try: entity_pk = int(entity) except ValueError as err: return HttpResponseBadRequest('Bad Request: {error}'.format(error=err)) # TODO: entities_to_map.values_list() doesn't return entities from selected page if entity_pk not in [e.pk for e in entities_to_map]: if entity_pk in entities.values_list('pk', flat=True): entities_to_map = list(entities_to_map) + list(entities.filter(pk=entity_pk)) translations = Translation.for_locale_project_paths(locale, project, paths) return JsonResponse({ 'entities': Entity.map_entities(locale, entities_to_map, visible_entities), 'has_next': has_next, 'stats': TranslatedResource.objects.stats(project, paths, locale), 'authors': translations.authors().serialize(), 'counts_per_minute': translations.counts_per_minute(), }, safe=False)
t = Translation.objects.get(entity=e, locale=l, author=request.user.email) if translation != '': log.debug("Translation updated.") t.string = translation t.save() return HttpResponse("updated") else: log.debug("Translation deleted.") t.delete() return HttpResponse("deleted") except Translation.DoesNotExist: """Save new translation.""" if translation != '': t = Translation(entity=e, locale=l, string=translation, author=request.user.email, date=datetime.datetime.now()) t.save() log.debug("Translation saved.") return HttpResponse("saved") else: log.debug("Translation not set.") return HttpResponse("not set") def _generate_properties_content(url, locale): """ Generate .properties file content. Args: pid: project ID locale: locale code Returns:
def pretranslate(project, locales=None, entities=None): """ Identifies strings without any translations and any suggestions. Engages TheAlgorithm (bug 1552796) to gather pretranslations. Stores pretranslations as suggestions (approved=False) to DB. :arg Project project: the project to be pretranslated :arg Queryset locales: the locales for the project to be pretranslated :arg Queryset entites: the entities for the project to be pretranslated :returns: None """ log.info("Fetching pretranslations for project {} started".format( project.name)) if locales: locales = project.locales.filter(pk__in=locales) else: locales = project.locales locales = (locales.filter(project_locale__readonly=False).distinct(). prefetch_project_locale(project)) if not entities: entities = Entity.objects.filter( resource__project=project, obsolete=False, ).prefetch_related("resource") # get available TranslatedResource pairs tr_pairs = (TranslatedResource.objects.filter( resource__project=project, locale__in=locales, ).annotate(locale_resource=Concat( "locale_id", V("-"), "resource_id", output_field=CharField())).values_list("locale_resource", flat=True).distinct()) # Fetch all distinct locale-entity pairs for which translation exists translated_entities = (Translation.objects.filter( locale__in=locales, entity__in=entities, ).annotate(locale_entity=Concat( "locale_id", V("-"), "entity_id", output_field=CharField())).values_list("locale_entity", flat=True).distinct()) translated_entities = list(translated_entities) translations = [] # To keep track of changed Locales and TranslatedResources # Also, their latest_translation and stats count locale_dict = {} tr_dict = {} tr_filter = [] index = -1 for locale in locales: log.info("Fetching pretranslations for locale {} started".format( locale.code)) for entity in entities: locale_entity = "{}-{}".format(locale.id, entity.id) locale_resource = "{}-{}".format(locale.id, entity.resource.id) if locale_entity in translated_entities or locale_resource not in tr_pairs: continue strings = get_translations(entity, locale) if not strings: continue for string, plural_form, user in strings: t = Translation( entity=entity, locale=locale, string=string, user=user, approved=False, active=True, plural_form=plural_form, ) translations.append(t) index += 1 if locale_resource not in tr_dict: tr_dict[locale_resource] = [index, 0] # Add query for fetching respective TranslatedResource. tr_filter.append( Q(locale__id=locale.id) & Q(resource__id=entity.resource.id)) if locale.code not in locale_dict: locale_dict[locale.code] = [locale, index, 0] # Update the latest translation index tr_dict[locale_resource][0] = index locale_dict[locale.code][1] = index # Increment number of translations (used to adjust stats) tr_dict[locale_resource][1] += 1 locale_dict[locale.code][2] += 1 log.info("Fetching pretranslations for locale {} done".format( locale.code)) if len(translations) == 0: return translations = Translation.objects.bulk_create(translations) # Update latest activity and unreviewed count for the project. project.latest_translation = translations[-1] project.unreviewed_strings += len(translations) project.save(update_fields=["latest_translation", "unreviewed_strings"]) # Update latest activity and unreviewed count for changed instances. update_changed_instances(tr_filter, tr_dict, locale_dict, translations) log.info("Fetching pretranslations for project {} done".format( project.name))
def update_translation(request): """Update entity translation for the specified locale and user. Note that this view is also used to approve a translation by the old Translate app. Once we migrate to Translate.Next, we'll want to rework this view to remove the bits about approving a translation, because that has been delegated to the `approve_translation` view. """ try: entity = request.POST['entity'] string = request.POST['translation'] locale = request.POST['locale'] plural_form = request.POST['plural_form'] original = request.POST['original'] ignore_warnings = request.POST.get('ignore_warnings', 'false') == 'true' approve = request.POST.get('approve', 'false') == 'true' force_suggestions = request.POST.get('force_suggestions', 'false') == 'true' paths = request.POST.getlist('paths[]') except MultiValueDictKeyError as e: return JsonResponse( { 'status': False, 'message': 'Bad Request: {error}'.format(error=e), }, status=400) try: e = Entity.objects.get(pk=entity) except Entity.DoesNotExist as e: return JsonResponse( { 'status': False, 'message': 'Bad Request: {error}'.format(error=e), }, status=400) try: locale = Locale.objects.get(code=locale) except Locale.DoesNotExist as e: return JsonResponse( { 'status': False, 'message': 'Bad Request: {error}'.format(error=e), }, status=400) if plural_form == "-1": plural_form = None user = request.user project = e.resource.project # Read-only translations cannot saved if utils.readonly_exists(project, locale): return JsonResponse( { 'status': False, 'message': 'Forbidden: This string is in read-only mode.', }, status=403) now = timezone.now() can_translate = (request.user.can_translate(project=project, locale=locale) and (not force_suggestions or approve)) translations = Translation.objects.filter(entity=e, locale=locale, plural_form=plural_form) same_translations = translations.filter(string=string).order_by( '-active', 'rejected', '-date') # If same translation exists in the DB, don't save it again. if utils.is_same(same_translations, can_translate): return JsonResponse({ 'same': True, }) # Check for errors. # Checks are disabled for the tutorial. use_checks = project.slug != 'tutorial' if use_checks: failed_checks = run_checks( e, locale.code, original, string, user.profile.quality_checks, ) if are_blocking_checks(failed_checks, ignore_warnings): return JsonResponse({ 'failedChecks': failed_checks, }) # Translations exist if len(translations) > 0: # Same translation exists if len(same_translations) > 0: t = same_translations[0] # If added by privileged user, approve and unfuzzy it if can_translate and (t.fuzzy or not t.approved): if not t.active: translations.filter(active=True).update(active=False) t.active = True t.approved = True t.fuzzy = False t.rejected = False t.rejected_user = None t.rejected_date = None if t.approved_user is None: t.approved_user = user t.approved_date = now t.save(failed_checks=failed_checks) return JsonResponse({ 'type': 'updated', 'translation': t.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, locale), }) # Different translation added else: t = Translation( entity=e, locale=locale, plural_form=plural_form, string=string, user=user, date=now, approved=can_translate, ) if can_translate: t.approved_user = user t.approved_date = now t.save(failed_checks=failed_checks) active_translation = e.reset_active_translation( locale=locale, plural_form=plural_form, ) return JsonResponse({ 'type': 'added', 'translation': active_translation.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, locale), }) # No translations saved yet else: t = Translation( entity=e, locale=locale, plural_form=plural_form, string=string, user=user, date=now, active=True, approved=can_translate, ) if can_translate: t.approved_user = user t.approved_date = now t.save(failed_checks=failed_checks) return JsonResponse({ 'type': 'saved', 'translation': t.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, locale), })
def update_translation(request, template=None): """Update entity translation for the specified locale and user.""" log.debug("Update entity translation for the specified locale and user.") if not request.is_ajax(): log.error("Non-AJAX request") raise Http404 if request.method != "POST": log.error("Non-POST request") raise Http404 try: entity = request.POST["entity"] string = request.POST["translation"] locale = request.POST["locale"] plural_form = request.POST["plural_form"] original = request.POST["original"] ignore_check = request.POST["ignore_check"] except MultiValueDictKeyError as error: log.error(str(error)) return HttpResponse("error") log.debug("Entity: " + entity) log.debug("Translation: " + string) log.debug("Locale: " + locale) try: e = Entity.objects.get(pk=entity) except Entity.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") try: l = Locale.objects.get(code__iexact=locale) except Locale.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") if plural_form == "-1": plural_form = None user = request.user if not request.user.is_authenticated(): if e.resource.project.pk != 1: log.error("Not authenticated") return HttpResponse("error") else: user = None try: quality_checks = UserProfile.objects.get(user=user).quality_checks except UserProfile.DoesNotExist as error: quality_checks = True ignore = False if ignore_check == "true" or not quality_checks: ignore = True now = timezone.now() can_localize = request.user.has_perm("base.can_localize") translations = Translation.objects.filter(entity=e, locale=l, plural_form=plural_form) # Newlines are not allowed in .lang files (bug 1190754) if e.resource.format == "lang" and "\n" in string: return HttpResponse("Newline characters are not allowed.") # Translations exist if len(translations) > 0: # Same translation exists try: t = translations.get(string=string) # If added by privileged user, approve and unfuzzy it if can_localize: # Unless there's nothing to be changed if t.user is not None and t.approved and t.approved_user and t.approved_date and not t.fuzzy: return HttpResponse("Same translation already exists.") warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings unapprove(translations) unfuzzy(translations) if t.user is None: t.user = user t.approved = True t.approved_date = timezone.now() t.fuzzy = False if t.approved_user is None: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() return HttpResponse( json.dumps({"type": "updated", "translation": t.serialize()}), content_type="application/json" ) # If added by non-privileged user, unfuzzy it else: if t.fuzzy: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings if t.user is None: t.user = user t.approved = False t.approved_user = None t.approved_date = None t.fuzzy = False if request.user.is_authenticated(): t.save() return HttpResponse( json.dumps({"type": "updated", "translation": t.serialize()}), content_type="application/json" ) return HttpResponse("Same translation already exists.") # Different translation added except: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings if can_localize: unapprove(translations) unfuzzy(translations) t = Translation( entity=e, locale=l, user=user, string=string, plural_form=plural_form, date=now, approved=can_localize ) if can_localize: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() active = get_translation(entity=e, locale=l, plural_form=plural_form) return HttpResponse( json.dumps({"type": "added", "translation": active.serialize()}), content_type="application/json" ) # No translations saved yet else: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings t = Translation( entity=e, locale=l, user=user, string=string, plural_form=plural_form, date=now, approved=can_localize ) if can_localize: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() return HttpResponse( json.dumps({"type": "saved", "translation": t.serialize()}), content_type="application/json" )
def dump_po(project, locale, relative_path): """Dump .po (gettext) file with relative path from database.""" locale_directory_path = get_locale_directory(project, locale)["path"] path = os.path.join(locale_directory_path, relative_path) po = polib.pofile(path) date = timezone.make_aware(datetime.min) newest = Translation() resource = Resource.objects.filter(project=project, path=relative_path) entities = Entity.objects.filter(resource=resource, obsolete=False) for entity in entities: entry = po.find(entity.string) if entry: if not entry.msgid_plural: try: translation = Translation.objects.filter( entity=entity, locale=locale, approved=True) \ .latest('date') entry.msgstr = translation.string if translation.date > date: date = translation.date newest = translation if 'fuzzy' in entry.flags: entry.flags.remove('fuzzy') except Translation.DoesNotExist as e: pass else: for i in range(0, 6): if i < (locale.nplurals or 1): try: translation = Translation.objects.filter( entity=entity, locale=locale, plural_form=i, approved=True).latest('date') entry.msgstr_plural[i] = translation.string if translation.date > date: date = translation.date newest = translation if 'fuzzy' in entry.flags: entry.flags.remove('fuzzy') except Translation.DoesNotExist as e: pass # Remove obsolete plural forms if exist else: if i in entry.msgstr_plural: del entry.msgstr_plural[i] # Update PO metadata if newest.id: if newest.user: po.metadata['PO-Revision-Date'] = newest.date po.metadata['Last-Translator'] = '%s <%s>' \ % (newest.user.first_name, newest.user.email) po.metadata['Language'] = locale.code.replace('-', '_') po.metadata['X-Generator'] = 'Pontoon' if locale.nplurals: po.metadata['Plural-Forms'] = 'nplurals=%s; plural=%s;' \ % (str(locale.nplurals), locale.plural_rule) po.save() log.debug("File updated: " + path)
def pretranslate(self, project_pk, locales=None, entities=None): """ Identifies strings without any translations and any suggestions. Engages TheAlgorithm (bug 1552796) to gather pretranslations. Stores pretranslations as suggestions (approved=False) to DB. :arg project_pk: the pk of the project to be pretranslated :arg Queryset locales: the locales for the project to be pretranslated :arg Queryset entites: the entities for the project to be pretranslated :returns: None """ project = Project.objects.get(pk=project_pk) log.info("Fetching pretranslations for project {} started".format( project.name)) if locales: locales = project.locales.filter(pk__in=locales) else: locales = project.locales locales = (locales.filter(project_locale__readonly=False).distinct(). prefetch_project_locale(project)) if not entities: entities = Entity.objects.filter( resource__project=project, obsolete=False, ).prefetch_related("resource") # get available TranslatedResource pairs tr_pairs = (TranslatedResource.objects.filter( resource__project=project, locale__in=locales, ).annotate(locale_resource=Concat( "locale_id", V("-"), "resource_id", output_field=CharField())).values_list("locale_resource", flat=True).distinct()) # Fetch all distinct locale-entity pairs for which translation exists translated_entities = (Translation.objects.filter( locale__in=locales, entity__in=entities, ).annotate(locale_entity=Concat( "locale_id", V("-"), "entity_id", output_field=CharField())).values_list("locale_entity", flat=True).distinct()) translated_entities = list(translated_entities) translations = [] # To keep track of changed TranslatedResources and their latest_translation tr_dict = {} tr_filter = [] index = -1 for locale in locales: log.info("Fetching pretranslations for locale {} started".format( locale.code)) for entity in entities: locale_entity = "{}-{}".format(locale.id, entity.id) locale_resource = "{}-{}".format(locale.id, entity.resource.id) if locale_entity in translated_entities or locale_resource not in tr_pairs: continue strings = get_translations(entity, locale) if not strings: continue for string, plural_form, user in strings: t = Translation( entity=entity, locale=locale, string=string, user=user, approved=False, fuzzy=True, active=True, plural_form=plural_form, ) index += 1 translations.append(t) if locale_resource not in tr_dict: tr_dict[locale_resource] = index # Add query for fetching respective TranslatedResource. tr_filter.append( Q(locale__id=locale.id) & Q(resource__id=entity.resource.id)) # Update the latest translation index tr_dict[locale_resource] = index log.info("Fetching pretranslations for locale {} done".format( locale.code)) if len(translations) == 0: return translations = Translation.objects.bulk_create(translations) # Run checks on all translations translation_pks = {translation.pk for translation in translations} bulk_run_checks( Translation.objects.for_checks().filter(pk__in=translation_pks)) # Mark translations as changed changed_entities = {} existing = ChangedEntityLocale.objects.values_list("entity", "locale").distinct() for t in translations: key = (t.entity.pk, t.locale.pk) # Remove duplicate changes to prevent unique constraint violation if key not in existing: changed_entities[key] = ChangedEntityLocale(entity=t.entity, locale=t.locale) ChangedEntityLocale.objects.bulk_create(changed_entities.values()) # Update latest activity and stats for changed instances. update_changed_instances(tr_filter, tr_dict, translations) log.info("Fetching pretranslations for project {} done".format( project.name))
def entities(request): """Get entities for the specified project, locale and paths.""" try: project = request.POST['project'] locale = request.POST['locale'] paths = request.POST.getlist('paths[]') limit = int(request.POST.get('limit', 50)) except (MultiValueDictKeyError, ValueError) as err: return HttpResponseBadRequest('Bad Request: {error}'.format(error=err)) project = get_object_or_404(Project, slug=project) locale = get_object_or_404(Locale, code__iexact=locale) filter_type = request.POST.get('filter', '') search = request.POST.get('search', '') exclude_entities = request.POST.getlist('excludeEntities[]', []) # Only return entities with provided IDs (batch editing) entity_ids = request.POST.getlist('entityIds[]', []) if entity_ids: entities = ( Entity.objects.filter(pk__in=entity_ids) .prefetch_resources_translations(locale) .distinct() .order_by('order') ) return JsonResponse({ 'entities': Entity.map_entities(locale, entities), 'stats': TranslatedResource.objects.stats(project, paths, locale), 'authors': Translation.authors(locale, project, paths).serialize(), }, safe=False) entities = Entity.for_project_locale( project, locale, paths, filter_type, search, exclude_entities ) # Only return a list of entity PKs (batch editing: select all) if request.POST.get('pkOnly', None): return JsonResponse({ 'entity_pks': list(entities.values_list('pk', flat=True)), }) visible_entities = [] # In-place view: load all entities if request.POST.get('inplaceEditor', None): has_next = False entities_to_map = Entity.for_project_locale( project, locale, paths, None, None, exclude_entities ) visible_entities = entities.values_list('pk', flat=True) # Out-of-context view: paginate entities else: paginator = Paginator(entities, limit) try: entities_page = paginator.page(1) except EmptyPage: return JsonResponse({ 'has_next': False, 'stats': {}, 'authors': [] }) has_next = entities_page.has_next() entities_to_map = entities_page.object_list # If requested entity not on the first page entity = request.POST.get('entity', None) if entity: try: entity_pk = int(entity) except ValueError as err: return HttpResponseBadRequest('Bad Request: {error}'.format(error=err)) # TODO: entities_to_map.values_list() doesn't return entities from selected page if entity_pk not in [e.pk for e in entities_to_map]: if entity_pk in entities.values_list('pk', flat=True): entities_to_map = list(entities_to_map) + list(entities.filter(pk=entity_pk)) return JsonResponse({ 'entities': Entity.map_entities(locale, entities_to_map, visible_entities), 'has_next': has_next, 'stats': TranslatedResource.objects.stats(project, paths, locale), 'authors': Translation.authors(locale, project, paths).serialize(), }, safe=False)
def update_translation(request, template=None): """Update entity translation for the specified locale and user.""" log.debug("Update entity translation for the specified locale and user.") if not request.is_ajax(): log.error("Non-AJAX request") raise Http404 if request.method != 'POST': log.error("Non-POST request") raise Http404 try: entity = request.POST['entity'] string = request.POST['translation'] locale = request.POST['locale'] plural_form = request.POST['plural_form'] original = request.POST['original'] ignore_check = request.POST['ignore_check'] except MultiValueDictKeyError as error: log.error(str(error)) return HttpResponse("error") log.debug("Entity: " + entity) log.debug("Translation: " + string) log.debug("Locale: " + locale) try: e = Entity.objects.get(pk=entity) except Entity.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") try: l = Locale.objects.get(code=locale) except Locale.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") if plural_form == "-1": plural_form = None user = request.user if not request.user.is_authenticated(): if e.resource.project.pk != 1: log.error("Not authenticated") return HttpResponse("error") else: user = None try: quality_checks = UserProfile.objects.get(user=user).quality_checks except UserProfile.DoesNotExist as error: quality_checks = True ignore = False if ignore_check == 'true' or not quality_checks: ignore = True now = datetime.datetime.now() can_localize = request.user.has_perm('base.can_localize') translations = Translation.objects.filter( entity=e, locale=l, plural_form=plural_form) # Translations exist if len(translations) > 0: # Same translation exists try: t = translations.get(string__iexact=string) # If added by privileged user, approve and unfuzzy it if can_localize: # Unless there's nothing to be changed if t.user is not None and t.approved and t.approved_user \ and t.approved_date and not t.fuzzy: return HttpResponse("Same translation already exist.") warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings unapprove(translations) unfuzzy(translations) if t.user is None: t.user = user t.approved = True t.fuzzy = False if t.approved_user is None: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() return HttpResponse(json.dumps({ 'type': 'updated', 'translation': t.serialize(), }), mimetype='application/json') # If added by non-privileged user, unfuzzy it else: if t.fuzzy: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings if t.user is None: t.user = user t.approved = False t.approved_user = None t.approved_date = None t.fuzzy = False if request.user.is_authenticated(): t.save() return HttpResponse(json.dumps({ 'type': 'updated', 'translation': t.serialize(), }), mimetype='application/json') return HttpResponse("Same translation already exist.") # Different translation added except: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings if can_localize: unapprove(translations) unfuzzy(translations) t = Translation( entity=e, locale=l, user=user, string=string, plural_form=plural_form, date=now, approved=can_localize) if can_localize: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() active = get_translation( entity=e, locale=l, plural_form=plural_form) return HttpResponse(json.dumps({ 'type': 'added', 'translation': active.serialize(), }), mimetype='application/json') # No translations saved yet else: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings t = Translation( entity=e, locale=l, user=user, string=string, plural_form=plural_form, date=now, approved=can_localize) if can_localize: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() return HttpResponse(json.dumps({ 'type': 'saved', 'translation': t.serialize(), }), mimetype='application/json')
def update_translation(request): """Update entity translation for the specified locale and user.""" try: entity = request.POST['entity'] string = request.POST['translation'] locale = request.POST['locale'] plural_form = request.POST['plural_form'] original = request.POST['original'] ignore_check = request.POST['ignore_check'] approve = json.loads(request.POST['approve']) paths = request.POST.getlist('paths[]') except MultiValueDictKeyError as e: return HttpResponseBadRequest('Bad Request: {error}'.format(error=e)) try: e = Entity.objects.get(pk=entity) except Entity.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") try: l = Locale.objects.get(code__iexact=locale) except Locale.DoesNotExist as error: log.error(str(error)) return HttpResponse("error") if plural_form == "-1": plural_form = None user = request.user project = e.resource.project if not request.user.is_authenticated(): if project.pk != 1: log.error("Not authenticated") return HttpResponse("error") else: user = None try: quality_checks = UserProfile.objects.get(user=user).quality_checks except UserProfile.DoesNotExist as error: quality_checks = True ignore = False if ignore_check == 'true' or not quality_checks: ignore = True now = timezone.now() can_translate = ( request.user.has_perm('base.can_translate_locale', l) and (not request.user.profile.force_suggestions or approve) ) translations = Translation.objects.filter( entity=e, locale=l, plural_form=plural_form) # Newlines are not allowed in .lang files (bug 1190754) if e.resource.format == 'lang' and '\n' in string: return HttpResponse('Newline characters are not allowed.') # Translations exist if len(translations) > 0: # Same translation exists try: t = translations.get(string=string) # If added by privileged user, approve and unfuzzy it if can_translate: # Unless there's nothing to be changed if t.user is not None and t.approved and t.approved_user \ and t.approved_date and not t.fuzzy: return JsonResponse({ 'same': True, 'message': 'Same translation already exists.', }) warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings translations.update(approved=False, approved_user=None, approved_date=None) translations.update(fuzzy=False) if t.user is None: t.user = user t.approved = True t.approved_date = timezone.now() t.fuzzy = False if t.approved_user is None: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() return JsonResponse({ 'type': 'updated', 'translation': t.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, l), 'authors': Translation.authors(l, project, paths).serialize(), }) # If added by non-privileged user, unfuzzy it else: if t.fuzzy: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings if t.user is None: t.user = user t.approved = False t.approved_user = None t.approved_date = None t.fuzzy = False if request.user.is_authenticated(): t.save() return JsonResponse({ 'type': 'updated', 'translation': t.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, l), 'authors': Translation.authors(l, project, paths).serialize(), }) return JsonResponse({ 'same': True, 'message': 'Same translation already exists.', }) # Different translation added except: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings if can_translate: translations.update(approved=False, approved_user=None, approved_date=None) translations.update(fuzzy=False) t = Translation( entity=e, locale=l, user=user, string=string, plural_form=plural_form, date=now, approved=can_translate) if can_translate: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() # Return active (approved or latest) translation try: active = translations.filter(approved=True).latest("date") except Translation.DoesNotExist: active = translations.latest("date") return JsonResponse({ 'type': 'added', 'translation': active.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, l), 'authors': Translation.authors(l, project, paths).serialize(), }) # No translations saved yet else: warnings = utils.quality_check(original, string, l, ignore) if warnings: return warnings t = Translation( entity=e, locale=l, user=user, string=string, plural_form=plural_form, date=now, approved=can_translate) if can_translate: t.approved_user = user t.approved_date = now if request.user.is_authenticated(): t.save() return JsonResponse({ 'type': 'saved', 'translation': t.serialize(), 'stats': TranslatedResource.objects.stats(project, paths, l), 'authors': Translation.authors(l, project, paths).serialize(), })
def create_translation(request): """ Create a new translation. """ form = forms.CreateTranslationForm(request.POST) if not form.is_valid(): problems = [] for field, errors in form.errors.items(): problems.append( 'Error validating field `{}`: "{}"'.format(field, " ".join(errors)) ) return JsonResponse( {"status": False, "message": "\n".join(problems)}, status=400 ) entity = form.cleaned_data["entity"] string = form.cleaned_data["translation"] locale = form.cleaned_data["locale"] plural_form = form.cleaned_data["plural_form"] original = form.cleaned_data["original"] ignore_warnings = form.cleaned_data["ignore_warnings"] approve = form.cleaned_data["approve"] force_suggestions = form.cleaned_data["force_suggestions"] paths = form.cleaned_data["paths"] machinery_sources = form.cleaned_data["machinery_sources"] project = entity.resource.project # Read-only translations cannot saved if utils.readonly_exists(project, locale): return JsonResponse( { "status": False, "message": "Forbidden: This string is in read-only mode.", }, status=403, ) translations = Translation.objects.filter( entity=entity, locale=locale, plural_form=plural_form, ) same_translations = translations.filter(string=string) # If same translation exists in the DB, don't save it again. if same_translations: return JsonResponse({"status": False, "same": True}) # Look for failed checks. # Checks are disabled for the tutorial. use_checks = project.slug != "tutorial" user = request.user first_contribution = user.is_new_contributor(locale) failed_checks = None if use_checks: failed_checks = run_checks( entity, locale.code, original, string, user.profile.quality_checks, ) if are_blocking_checks(failed_checks, ignore_warnings): return JsonResponse({"status": False, "failedChecks": failed_checks}) now = timezone.now() can_translate = user.can_translate(project=project, locale=locale) and ( not force_suggestions or approve ) translation = Translation( entity=entity, locale=locale, plural_form=plural_form, string=string, user=user, date=now, approved=can_translate, machinery_sources=machinery_sources, ) if can_translate: translation.approved_user = user translation.approved_date = now translation.save(failed_checks=failed_checks) log_action(ActionLog.ActionType.TRANSLATION_CREATED, user, translation=translation) if translations: translation = entity.reset_active_translation( locale=locale, plural_form=plural_form, ) # When user makes their first contribution to the team, notify team managers if first_contribution: desc = """ <a href="{user_href}">{user}</a> has made their first contribution to <a href="{locale_href}">{locale} ({locale_code})</a>. Please welcome them to the team, and make sure to <a href="{review_href}">review their suggestions</a>. """.format( user=user.name_or_email, user_href=reverse( "pontoon.contributors.contributor.username", kwargs={ "username": user.username, }, ), locale=locale.name, locale_code=locale.code, locale_href=reverse( "pontoon.teams.team", kwargs={ "locale": locale.code, }, ), review_href=reverse( "pontoon.translate", kwargs={ "locale": locale.code, "project": project.slug, "resource": entity.resource.path, }, ) + f"?string={entity.pk}", ) for manager in locale.managers_group.user_set.filter( profile__new_contributor_notifications=True ): notify.send( sender=manager, recipient=manager, verb="has reviewed suggestions", # Triggers render of description only description=desc, ) return JsonResponse( { "status": True, "translation": translation.serialize(), "stats": TranslatedResource.objects.stats(project, paths, locale), } )