def get_last_content_change(self, silent=False): """Wrapper to get last content change metadata. Used when commiting pending changes, needs to handle and report inconsistencies from past releases. """ from weblate.auth.models import get_anonymous try: change = self.recent_content_changes[0] return change.author or get_anonymous(), change.timestamp except IndexError: return get_anonymous(), timezone.now()
def get_last_content_change(self, request): """Wrapper to get last content change metadata Used when commiting pending changes, needs to handle and report inconsistencies from past releases. """ from weblate.auth.models import get_anonymous try: change = self.change_set.content().order_by('-timestamp')[0] return change.author or get_anonymous(), change.timestamp except IndexError as error: report_error(error, request, level='error') return get_anonymous(), timezone.now()
def get_last_content_change(self, request, silent=False): """Wrapper to get last content change metadata Used when commiting pending changes, needs to handle and report inconsistencies from past releases. """ from weblate.auth.models import get_anonymous try: change = self.change_set.content().order_by('-timestamp')[0] return change.author or get_anonymous(), change.timestamp except IndexError as error: if not silent: report_error(error, request, level='error') return get_anonymous(), timezone.now()
def get_last_content_change(self, silent=False): """Wrapper to get last content change metadata. Used when commiting pending changes, needs to handle and report inconsistencies from past releases. """ from weblate.auth.models import get_anonymous try: change = self.change_set.content().order_by("-timestamp")[0] return change.author or get_anonymous(), change.timestamp except IndexError: if not silent: report_error(level="error") return get_anonymous(), timezone.now()
def add_units( self, request, batch: List[Tuple[str, Union[str, List[str]], Optional[Union[str, List[str]]]]], ): from weblate.auth.models import get_anonymous user = request.user if request else get_anonymous() with self.component.repository.lock: self.component.commit_pending("new unit", user) previous_revision = (self.component.repository.last_revision, ) for translation in self.get_store_change_translations(): for context, source, target in batch: translation.store.new_unit(context, source, target, skip_save=True) Change.objects.create( translation=translation, action=Change.ACTION_NEW_UNIT, target=source, user=user, author=user, ) translation.store.save() translation.drop_store_cache() translation.git_commit(user, user.get_author_name(), store_hash=False) self.handle_store_change(request, user, previous_revision) if self.is_source: self.component.sync_terminology()
def test_protect_project(self): middleware = RequireLoginMiddleware() request = HttpRequest() request.user = User() request.META['SERVER_NAME'] = 'testserver' request.META['SERVER_PORT'] = '80' # No protection for not protected path self.assertIsNone( middleware.process_view(request, self.view_method, (), {}) ) request.path = '/project/foo/' # No protection for protected path and logged in user self.assertIsNone( middleware.process_view(request, self.view_method, (), {}) ) # Protection for protected path and not logged in user request.user = get_anonymous() self.assertIsInstance( middleware.process_view(request, self.view_method, (), {}), HttpResponseRedirect ) # No protection for login and not logged in user request.path = '/accounts/login/' self.assertIsNone( middleware.process_view(request, self.view_method, (), {}) )
def send_invitation(request: HttpRequest, project_name: str, user: User): """Send invitation to user to join project.""" fake = HttpRequest() fake.user = get_anonymous() fake.method = "POST" fake.session = create_session() fake.session["invitation_context"] = { "from_user": request.user.full_name, "project_name": project_name, } fake.POST["email"] = user.email fake.META = request.META store_userid(fake, invite=True) # Make sure the email backend is there for the invitation email_auth = "social_core.backends.email.EmailAuth" has_email = email_auth in settings.AUTHENTICATION_BACKENDS backup_backends = settings.AUTHENTICATION_BACKENDS backup_cache = social_core.backends.utils.BACKENDSCACHE backup_social = social_django.utils.BACKENDS if not has_email: social_core.backends.utils.BACKENDSCACHE["email"] = EmailAuth settings.AUTHENTICATION_BACKENDS += (email_auth,) # Send invitation complete(fake, "email") # Revert temporary settings override if not has_email: social_core.backends.utils.BACKENDSCACHE = backup_cache settings.AUTHENTICATION_BACKENDS = backup_backends social_django.utils.BACKENDS = backup_social
def new_unit( self, request, key: Optional[str], value: Optional[str], batch: Optional[Dict[str, str]] = None, ): from weblate.auth.models import get_anonymous user = request.user if request else get_anonymous() with self.component.repository.lock: self.component.commit_pending("new unit", user) if batch: for batch_key, batch_value in batch.items(): self.store.new_unit(batch_key, batch_value) Change.objects.create( translation=self, action=Change.ACTION_NEW_UNIT, target=batch_value, user=user, author=user, ) else: self.store.new_unit(key, value) Change.objects.create( translation=self, action=Change.ACTION_NEW_UNIT, target=value, user=user, author=user, ) self.component.create_translations(request=request) self.git_commit(user, user.get_author_name())
def cleanup_suggestions(): # Process suggestions anonymous_user = get_anonymous() suggestions = Suggestion.objects.prefetch_related('project', 'language') for suggestion in suggestions.iterator(): with transaction.atomic(): # Remove suggestions with same text as real translation units = Unit.objects.filter( content_hash=suggestion.content_hash, translation__language=suggestion.language, translation__component__project=suggestion.project, ) if not units.exclude(target=suggestion.target).exists(): suggestion.delete_log(anonymous_user, change=Change.ACTION_SUGGESTION_CLEANUP) continue # Remove duplicate suggestions sugs = Suggestion.objects.filter( content_hash=suggestion.content_hash, language=suggestion.language, project=suggestion.project, target=suggestion.target).exclude(id=suggestion.id) if sugs.exists(): suggestion.delete_log(anonymous_user, change=Change.ACTION_SUGGESTION_CLEANUP)
def cleanup_suggestions(): # Process suggestions anonymous_user = get_anonymous() suggestions = Suggestion.objects.prefetch_related('project', 'language') for suggestion in suggestions.iterator(): with transaction.atomic(): # Remove suggestions with same text as real translation units = Unit.objects.filter( content_hash=suggestion.content_hash, translation__language=suggestion.language, translation__component__project=suggestion.project, ) if not units.exclude(target=suggestion.target).exists(): suggestion.delete_log( anonymous_user, change=Change.ACTION_SUGGESTION_CLEANUP ) continue # Remove duplicate suggestions sugs = Suggestion.objects.filter( content_hash=suggestion.content_hash, language=suggestion.language, project=suggestion.project, target=suggestion.target ).exclude( id=suggestion.id ) if sugs.exists(): suggestion.delete_log( anonymous_user, change=Change.ACTION_SUGGESTION_CLEANUP )
def cleanup_suggestions(): # Process suggestions anonymous_user = get_anonymous() suggestions = Suggestion.objects.prefetch_related("unit") for suggestion in suggestions: with transaction.atomic(): # Remove suggestions with same text as real translation if ( suggestion.unit.target == suggestion.target and suggestion.unit.translated ): suggestion.delete_log( anonymous_user, change=Change.ACTION_SUGGESTION_CLEANUP ) continue # Remove duplicate suggestions sugs = Suggestion.objects.filter( unit=suggestion.unit, target=suggestion.target ).exclude(id=suggestion.id) # Do not rely on the SQL as MySQL compares strings case insensitive for other in sugs: if other.target == suggestion.target: suggestion.delete_log( anonymous_user, change=Change.ACTION_SUGGESTION_CLEANUP ) break
def cleanup_suggestions(): # Process suggestions anonymous_user = get_anonymous() suggestions = Suggestion.objects.prefetch_related('project', 'language') for suggestion in suggestions.iterator(): with transaction.atomic(): # Remove suggestions with same text as real translation is_different = False # Do not rely on the SQL as MySQL compares strings case insensitive for unit in suggestion.related_units: if unit.target != suggestion.target: is_different = True break if not is_different: suggestion.delete_log( anonymous_user, change=Change.ACTION_SUGGESTION_CLEANUP ) continue # Remove duplicate suggestions sugs = Suggestion.objects.filter( content_hash=suggestion.content_hash, language=suggestion.language, project=suggestion.project, target=suggestion.target, ).exclude(id=suggestion.id) # Do not rely on the SQL as MySQL compares strings case insensitive for other in sugs: if other.target == suggestion.target: suggestion.delete_log( anonymous_user, change=Change.ACTION_SUGGESTION_CLEANUP ) break
def handle(self, *args, **options): try: addon_class = ADDONS[options["addon"]] except KeyError: raise CommandError("Addon not found: {}".format(options["addon"])) addon = addon_class() try: configuration = json.loads(options["configuration"]) except ValueError as error: raise CommandError(f"Invalid addon configuration: {error}") try: user = User.objects.filter(is_superuser=True)[0] except IndexError: user = get_anonymous() for component in self.get_components(*args, **options): if addon.has_settings: form = addon.get_add_form(None, component, data=configuration) self.validate_form(form) addons = Addon.objects.filter_component(component).filter( name=addon.name) if addons: if options["update"]: for addon_component in addons: addon_component.addon.configure(configuration) self.stdout.write(f"Successfully updated on {component}") else: self.stderr.write(f"Already installed on {component}") continue if not addon.can_install(component, user): self.stderr.write(f"Can not install on {component}") continue addon.create(component, configuration=configuration) self.stdout.write(f"Successfully installed on {component}")
def add(self, unit, target, request, vote=False): """Create new suggestion for this unit.""" from weblate.auth.models import get_anonymous user = request.user if request else get_anonymous() if unit.translated and unit.target == target: return False same_suggestions = self.filter( target=target, content_hash=unit.content_hash, language=unit.translation.language, project=unit.translation.component.project, ) # Do not rely on the SQL as MySQL compares strings case insensitive for same in same_suggestions: if same.target == target: if same.user == user or not vote: return False same.add_vote(unit.translation, request, Vote.POSITIVE) return False # Create the suggestion suggestion = self.create( target=target, content_hash=unit.content_hash, language=unit.translation.language, project=unit.translation.component.project, user=user, userdetails={ 'address': get_ip_address(request) if request else '', 'agent': request.META.get('HTTP_USER_AGENT', '') if request else '', }, ) # Record in change for aunit in suggestion.related_units: Change.objects.create( unit=aunit, suggestion=suggestion, action=Change.ACTION_SUGGESTION, user=user, target=target, author=user, ) # Add unit vote if vote: suggestion.add_vote(unit.translation, request, Vote.POSITIVE) # Update suggestion stats if user is not None: user.profile.suggested += 1 user.profile.save() return True
def test_acl_disable(self): """Test disabling ACL.""" response = self.client.get(self.access_url) self.assertEqual(response.status_code, 404) self.project.access_control = Project.ACCESS_PUBLIC self.project.save() self.assertTrue(get_anonymous().can_access_project(self.project)) response = self.client.get(self.access_url) self.assertEqual(response.status_code, 403) response = self.client.get(self.translate_url) self.assertContains(response, 'type="submit" name="save"')
def check_avatars(app_configs, **kwargs): from weblate.auth.models import get_anonymous from weblate.accounts.avatar import download_avatar_image if not settings.ENABLE_AVATARS: return [] try: download_avatar_image(get_anonymous(), 32) return [] except (IOError, CertificateError) as error: return [weblate_check("weblate.E018", f"Failed to download avatar: {error}")]
def test_acl_protected(self): """Test ACL protected project.""" response = self.client.get(self.access_url) self.assertEqual(response.status_code, 404) self.project.access_control = Project.ACCESS_PROTECTED self.project.save() self.assertTrue(get_anonymous().can_access_project(self.project)) response = self.client.get(self.access_url) self.assertEqual(response.status_code, 403) response = self.client.get(self.translate_url) self.assertContains( response, "Insufficient privileges for saving translations.")
def send_invitation(request, project, user): fake = HttpRequest() fake.user = get_anonymous() fake.method = 'POST' fake.session = create_session() fake.session['invitation_context'] = { 'from_user': request.user.full_name, 'project_name': project.name, } fake.POST['email'] = user.email fake.META = request.META store_userid(fake, invite=True) complete(fake, 'email')
def get_user(request): """Based on django.contrib.auth.middleware.get_user. Adds handling of anonymous user which is stored in database. """ # pylint: disable=protected-access if not hasattr(request, "_cached_user"): user = auth.get_user(request) if isinstance(user, AnonymousUser): user = get_anonymous() request._cached_user = user return request._cached_user
def get_user(request): """Based on django.contrib.auth.middleware.get_user Adds handling of anonymous user which is stored in database. """ # pylint: disable=protected-access if not hasattr(request, '_cached_user'): user = auth.get_user(request) if isinstance(user, AnonymousUser): user = get_anonymous() request._cached_user = user return request._cached_user
def send_invitation(request, project, user): fake = HttpRequest() fake.user = get_anonymous() fake.method = "POST" fake.session = create_session() fake.session["invitation_context"] = { "from_user": request.user.full_name, "project_name": project.name, } fake.POST["email"] = user.email fake.META = request.META store_userid(fake, invite=True) complete(fake, "email")
def handle(self, *args, **options): try: addon = ADDONS[options['addon']] except KeyError: raise CommandError('Addon not found: {}'.format(options['addon'])) try: configuration = json.loads(options['configuration']) except ValueError as error: raise CommandError('Invalid addon configuration: {}'.format(error)) if addon.has_settings: form = addon.get_add_form(None, data=configuration) if not form.is_valid(): for error in form.non_field_errors(): self.stderr.write(error) for field in form: for error in field.errors: self.stderr.write( 'Error in {}: {}'.format(field.name, error) ) raise CommandError( 'Invalid addon configuration!' ) try: user = User.objects.filter(is_superuser=True)[0] except IndexError: user = get_anonymous() for component in self.get_components(*args, **options): addons = Addon.objects.filter_component(component).filter( name=addon.name ) if addons.exists(): if options['update']: addons.update(configuration=configuration) self.stdout.write( 'Successfully updated on {}'.format(component) ) else: self.stderr.write( 'Already installed on {}'.format(component) ) continue if not addon.can_install(component, user): self.stderr.write('Can not install on {}'.format(component)) continue addon.create(component, configuration=configuration) self.stdout.write( 'Successfully installed on {}'.format(component) )
def handle(self, *args, **options): try: addon = ADDONS[options['addon']]() except KeyError: raise CommandError('Addon not found: {}'.format(options['addon'])) try: configuration = json.loads(options['configuration']) except ValueError as error: raise CommandError('Invalid addon configuration: {}'.format(error)) if addon.has_settings: form = addon.get_add_form(None, data=configuration) if not form.is_valid(): for error in form.non_field_errors(): self.stderr.write(error) for field in form: for error in field.errors: self.stderr.write( 'Error in {}: {}'.format(field.name, error) ) raise CommandError( 'Invalid addon configuration!' ) try: user = User.objects.filter(is_superuser=True)[0] except IndexError: user = get_anonymous() for component in self.get_components(*args, **options): addons = Addon.objects.filter_component(component).filter( name=addon.name ) if addons.exists(): if options['update']: addons.update(configuration=configuration) self.stdout.write( 'Successfully updated on {}'.format(component) ) else: self.stderr.write( 'Already installed on {}'.format(component) ) continue if not addon.can_install(component, user): self.stderr.write('Can not install on {}'.format(component)) continue addon.create(component, configuration=configuration) self.stdout.write( 'Successfully installed on {}'.format(component) )
def add(self, unit, target, request, vote=False): """Create new suggestion for this unit.""" from weblate.auth.models import get_anonymous user = request.user if request else get_anonymous() if unit.translated and unit.target == target: return False same_suggestions = self.filter(target=target, unit=unit) # Do not rely on the SQL as MySQL compares strings case insensitive for same in same_suggestions: if same.target == target: if same.user == user or not vote: return False same.add_vote(request, Vote.POSITIVE) return False # Create the suggestion suggestion = self.create( target=target, unit=unit, user=user, userdetails={ "address": get_ip_address(request), "agent": get_user_agent_raw(request), }, ) # Record in change Change.objects.create( unit=unit, suggestion=suggestion, action=Change.ACTION_SUGGESTION, user=user, target=target, author=user, ) # Add unit vote if vote: suggestion.add_vote(request, Vote.POSITIVE) # Update suggestion stats if user is not None: user.profile.suggested += 1 user.profile.save() return suggestion
def check_avatars(app_configs, **kwargs): from weblate.auth.models import get_anonymous from weblate.accounts.avatar import download_avatar_image if not settings.ENABLE_AVATARS: return [] try: download_avatar_image(get_anonymous(), 32) return [] except (IOError, CertificateError) as error: return [ Critical( 'Failed to download avatar: {}'.format(error), hint=get_doc_url('admin/optionals', 'avatars'), id='weblate.E018', ) ]
def delete_unit(self, request, unit): from weblate.auth.models import get_anonymous component = self.component user = request.user if request else get_anonymous() with component.repository.lock: component.commit_pending("delete unit", user) try: pounit, add = self.store.find_unit(unit.context, unit.source) except UnitNotFound: return if add: return extra_files = self.store.remove_unit(pounit.unit) self.addon_commit_files.extend(extra_files) self.handle_store_change(request, user)
def get_user(request): """Based on django.contrib.auth.middleware.get_user Adds handling of anonymous user which is stored in database. """ # pylint: disable=protected-access if not hasattr(request, '_cached_user'): user = auth.get_user(request) if isinstance(user, AnonymousUser): user = get_anonymous() # Set short expiry for anonymous sessions request.session.set_expiry(2200) else: request.session.set_expiry(None) request._cached_user = user return request._cached_user
def delete_unit(self, request, unit): from weblate.auth.models import get_anonymous component = self.component user = request.user if request else get_anonymous() with component.repository.lock: component.commit_pending("delete unit", user) try: pounit, add = self.store.find_unit(unit.context, unit.source) except UnitNotFound: return if add: return extra_files = self.store.remove_unit(pounit.unit) self.addon_commit_files.extend(extra_files) self.git_commit(user, user.get_author_name()) component.create_translations(request=request, force=True)
def add_units(self, request, batch: Dict[str, Union[str, List[str]]]): from weblate.auth.models import get_anonymous user = request.user if request else get_anonymous() with self.component.repository.lock: self.component.commit_pending("new unit", user) for key, value in batch.items(): self.store.new_unit(key, value) Change.objects.create( translation=self, action=Change.ACTION_NEW_UNIT, target=value, user=user, author=user, ) self.git_commit(user, user.get_author_name()) self.component.create_translations(request=request)
def get_user(request): """Based on django.contrib.auth.middleware.get_user Adds handling of anonymous user which is stored in database. """ # pylint: disable=protected-access if not hasattr(request, '_cached_user'): user = auth.get_user(request) if isinstance(user, AnonymousUser): user = get_anonymous() # Set short expiry for anonymous sessions request.session.set_expiry(1800) else: request.session.set_expiry(None) request._cached_user = user return request._cached_user
def delete_unit(self, request, unit): from weblate.auth.models import get_anonymous component = self.component user = request.user if request else get_anonymous() with component.repository.lock: component.commit_pending("delete unit", user) previous_revision = self.component.repository.last_revision for translation in self.get_store_change_translations(): try: pounit, add = translation.store.find_unit(unit.context, unit.source) except UnitNotFound: return if add: return extra_files = translation.store.remove_unit(pounit.unit) translation.addon_commit_files.extend(extra_files) translation.drop_store_cache() translation.git_commit(user, user.get_author_name(), store_hash=False) self.handle_store_change(request, user, previous_revision)
def test_protect_project(self): middleware = RequireLoginMiddleware() request = HttpRequest() request.user = User() request.META['SERVER_NAME'] = 'testserver' request.META['SERVER_PORT'] = '80' # No protection for not protected path self.assertIsNone(middleware.process_view(request, self.view_method, (), {})) request.path = '/project/foo/' # No protection for protected path and logged in user self.assertIsNone(middleware.process_view(request, self.view_method, (), {})) # Protection for protected path and not logged in user request.user = get_anonymous() self.assertIsInstance( middleware.process_view(request, self.view_method, (), {}), HttpResponseRedirect, ) # No protection for login and not logged in user request.path = '/accounts/login/' self.assertIsNone(middleware.process_view(request, self.view_method, (), {}))
def test_protect_project(self): middleware = RequireLoginMiddleware() request = HttpRequest() request.user = User() request.META["SERVER_NAME"] = "testserver" request.META["SERVER_PORT"] = "80" # No protection for not protected path self.assertIsNone(middleware.process_view(request, self.view_method, (), {})) request.path = "/project/foo/" # No protection for protected path and signed in user self.assertIsNone(middleware.process_view(request, self.view_method, (), {})) # Protection for protected path and not signed in user request.user = get_anonymous() self.assertIsInstance( middleware.process_view(request, self.view_method, (), {}), HttpResponseRedirect, ) # No protection for login and not signed in user request.path = "/accounts/login/" self.assertIsNone(middleware.process_view(request, self.view_method, (), {}))
def add_units( self, request, batch: List[Tuple[Union[str, List[str]], Union[str, List[str]], Optional[str]]], ): from weblate.auth.models import get_anonymous user = request.user if request else get_anonymous() with self.component.repository.lock: self.component.commit_pending("new unit", user) for context, source, target in batch: self.store.new_unit(context, source, target) Change.objects.create( translation=self, action=Change.ACTION_NEW_UNIT, target=source, user=user, author=user, ) self.handle_store_change(request, user)
def cleanup_suggestions(): # Process suggestions anonymous_user = get_anonymous() suggestions = Suggestion.objects.prefetch_related('project', 'language') for suggestion in suggestions.iterator(): with transaction.atomic(): # Remove suggestions with same text as real translation is_different = False # Do not rely on the SQL as MySQL compares strings case insensitive for unit in suggestion.related_units: if unit.target != suggestion.target: is_different = True break if not is_different: suggestion.delete_log( anonymous_user, change=Change.ACTION_SUGGESTION_CLEANUP ) continue # Remove duplicate suggestions sugs = Suggestion.objects.filter( content_hash=suggestion.content_hash, language=suggestion.language, project=suggestion.project, target=suggestion.target ).exclude( id=suggestion.id ) # Do not rely on the SQL as MySQL compares strings case insensitive for other in sugs: if other.target == suggestion.target: suggestion.delete_log( anonymous_user, change=Change.ACTION_SUGGESTION_CLEANUP ) break
def invite_user(request, project): """Invite user to a project.""" obj, form = check_user_form(request, project, True, form_class=InviteUserForm) if form is not None: try: user = form.save() obj.add_user(user) Change.objects.create( project=obj, action=Change.ACTION_INVITE_USER, user=request.user, details={'username': user.username}, ) fake = HttpRequest() fake.user = get_anonymous() fake.method = 'POST' fake.session = create_session() fake.session['invitation_context'] = { 'from_user': request.user.full_name, 'project_name': obj.name, } fake.POST['email'] = form.cleaned_data['email'] fake.META = request.META store_userid(fake, invite=True) complete(fake, 'email') messages.success(request, _('User has been invited to this project.')) except Group.DoesNotExist: messages.error(request, _('Failed to find group to add a user!')) return redirect( 'manage-access', project=obj.slug, )
def notify_connect( strategy, details, backend, user, social, new_association=False, is_new=False, **kwargs, ): """Notify about adding new link.""" # Adjust possibly pending email confirmation audit logs AuditLog.objects.filter( user=get_anonymous(), activity="sent-email", params={ "email": details["email"] }, ).update(user=user) if user and not is_new: if new_association: action = "auth-connect" else: action = "login" adjust_session_expiry(strategy.request) AuditLog.objects.create( user, strategy.request, action, method=backend.name, name=social.uid, ) # Remove partial pipeline session = strategy.request.session if PARTIAL_TOKEN_SESSION_NAME in session: strategy.really_clean_partial_pipeline( session[PARTIAL_TOKEN_SESSION_NAME])
def cleanup_database(self): """Cleanup the database""" anonymous_user = get_anonymous() with transaction.atomic(): projects = list(Project.objects.values_list('id', flat=True)) for pk in projects: with transaction.atomic(): # List all current unit content_hashs units = Unit.objects.filter( translation__component__project__pk=pk ).values('content_hash').distinct() # Remove source comments referring to deleted units Comment.objects.filter( language=None, project__pk=pk ).exclude( content_hash__in=units ).delete() # Remove source checks referring to deleted units Check.objects.filter( language=None, project__pk=pk ).exclude( content_hash__in=units ).delete() for lang in Language.objects.all(): with transaction.atomic(): # Remove checks referring to deleted or not translated # units translatedunits = Unit.objects.filter( translation__language=lang, state__gte=STATE_TRANSLATED, translation__component__project__pk=pk ).values('content_hash').distinct() Check.objects.filter( language=lang, project__pk=pk ).exclude( content_hash__in=translatedunits ).delete() # List current unit content_hashs units = Unit.objects.filter( translation__language=lang, translation__component__project__pk=pk ).values('content_hash').distinct() # Remove suggestions referring to deleted units Suggestion.objects.filter( language=lang, project__pk=pk ).exclude( content_hash__in=units ).delete() # Remove translation comments referring to deleted units Comment.objects.filter( language=lang, project__pk=pk ).exclude( content_hash__in=units ).delete() # Process suggestions all_suggestions = Suggestion.objects.filter( language=lang, project__pk=pk ) for sug in all_suggestions.iterator(): # Remove suggestions with same text as real translation units = Unit.objects.filter( content_hash=sug.content_hash, translation__language=lang, translation__component__project__pk=pk, ) if not units.exclude(target=sug.target).exists(): sug.delete_log( anonymous_user, Change.ACTION_SUGGESTION_CLEANUP ) continue # Remove duplicate suggestions sugs = Suggestion.objects.filter( content_hash=sug.content_hash, language=lang, project__pk=pk, target=sug.target ).exclude( id=sug.id ) if sugs.exists(): sug.delete_log( anonymous_user, Change.ACTION_SUGGESTION_CLEANUP )