Example #1
0
def sync_resources(db_project, now, force, no_pull):
    # Pull source repository
    if no_pull:
        source_repo_changed = True  # Assume changed
    else:
        source_repo_changed = pull_changes(db_project, source_only=True)

    # If the only repo hasn't changed since the last sync and there are
    # no Pontoon-side changes for this project, quit early.
    if not force and not db_project.needs_sync and not source_repo_changed and db_project.has_single_repo:
        log.info('Skipping project {0}, no changes detected.'.format(db_project.slug))
        return False

    if force or source_repo_changed:
        try:
            project_changes, obsolete_vcs_resources, new_paths = update_originals(db_project, now, full_scan=force)
        except MissingSourceDirectoryError as e:
            log.error(e)
            return False

        if not db_project.has_single_repo:
            db_project.source_repository.set_last_synced_revisions()
        log.info('Synced resources for project {0}.'.format(db_project.slug))

    else:
        project_changes, obsolete_vcs_resources, new_paths = None, None, None
        log.info('Skipping syncing resources for project {0}, no changes detected.'.format(db_project.slug))

    return {
        'project_changes': project_changes,
        'obsolete_vcs_resources': obsolete_vcs_resources,
        'new_paths': new_paths,
    }
Example #2
0
def sync_resources(db_project, now, force, no_pull):
    # Pull source repository
    if no_pull:
        source_repo_changed = True  # Assume changed
    else:
        source_repo_changed, _ = pull_changes(db_project, source_only=True)

    # If the only repo hasn't changed since the last sync and there are
    # no Pontoon-side changes for this project, quit early.
    if not force and not db_project.needs_sync and not source_repo_changed and db_project.has_single_repo:
        log.info('Skipping project {0}, no changes detected.'.format(db_project.slug))
        return False

    if force or source_repo_changed:
        try:
            project_changes, obsolete_vcs_resources, new_paths = update_originals(db_project, now, full_scan=force)
        except MissingSourceDirectoryError as e:
            log.error(e)
            return False

        if not db_project.has_single_repo:
            db_project.source_repository.set_last_synced_revisions()
        log.info('Synced resources for project {0}.'.format(db_project.slug))

    else:
        project_changes, obsolete_vcs_resources, new_paths = None, None, None
        log.info('Skipping syncing resources for project {0}, no changes detected.'.format(db_project.slug))

    return {
        'project_changes': project_changes,
        'obsolete_vcs_resources': obsolete_vcs_resources,
        'new_paths': new_paths,
    }
Example #3
0
def sync_project(self,
                 project_pk,
                 sync_log_pk,
                 no_pull=False,
                 no_commit=False,
                 force=False):
    """Fetch the project with the given PK and perform sync on it."""
    db_project = get_or_fail(
        Project,
        pk=project_pk,
        message='Could not sync project with pk={0}, not found.'.format(
            project_pk))
    sync_log = get_or_fail(
        SyncLog,
        pk=sync_log_pk,
        message=(
            'Could not sync project {0}, log with pk={1} not found.'.format(
                db_project.slug, sync_log_pk)))

    log.info('Syncing project {0}.'.format(db_project.slug))

    # Mark "now" at the start of sync to avoid messing with
    # translations submitted during sync.
    now = timezone.now()

    project_sync_log = ProjectSyncLog.objects.create(sync_log=sync_log,
                                                     project=db_project,
                                                     start_time=now)

    if not no_pull:
        repos_changed = pull_changes(db_project)
    else:
        repos_changed = True  # Assume changed.

    # If the repos haven't changed since the last sync and there are
    # no Pontoon-side changes for this project, quit early.
    if not force and not repos_changed and not db_project.needs_sync:
        log.info('Skipping project {0}, no changes detected.'.format(
            db_project.slug))

        project_sync_log.skipped = True
        project_sync_log.skipped_end_time = timezone.now()
        project_sync_log.save(update_fields=('skipped', 'skipped_end_time'))

        return

    obsolete_vcs = perform_sync_project(db_project, now)

    for repo in db_project.repositories.all():
        sync_project_repo.delay(project_pk,
                                repo.pk,
                                project_sync_log.pk,
                                now,
                                obsolete_vcs,
                                no_pull=no_pull,
                                no_commit=no_commit)

    log.info('Synced resources for project {0}.'.format(db_project.slug))
Example #4
0
    def test_unsure_changes(self):
        """
        If any of the repos returns None as a revision number, consider
        the VCS as changed even if the revisions match the last sync.
        """
        self.mock_repo_pull.return_value = {'single_locale': None}
        self.repository.last_synced_revisions = {'single_locale': None}
        self.repository.save()

        assert_true(pull_changes(self.db_project))
Example #5
0
 def test_basic(self):
     """
     Pull_changes should call repo.pull for each repo for the
     project, save the return value to repo.last_synced_revisions,
     and return whether any changes happened in VCS.
     """
     self.mock_repo_pull.return_value = {'single_locale': 'asdf'}
     assert_true(pull_changes(self.db_project))
     self.repository.refresh_from_db()
     assert_equal(self.repository.last_synced_revisions, {'single_locale': 'asdf'})
Example #6
0
    def test_unchanged(self):
        """
        If the revisions returned by repo.pull match those from the last
        sync, consider the VCS unchanged and return False.
        """
        self.mock_repo_pull.return_value = {'single_locale': 'asdf'}
        self.repository.last_synced_revisions = {'single_locale': 'asdf'}
        self.repository.save()

        assert_false(pull_changes(self.db_project))
Example #7
0
 def test_unchanged(self):
     """
     If the revisions returned by repo.pull match those from the last
     sync, consider the VCS unchanged and return False.
     """
     self.mock_repo_pull.return_value = {'single_locale': 'asdf'}
     self.repository.last_synced_revisions = {'single_locale': 'asdf'}
     self.repository.save()
     has_changed, _ = pull_changes(self.db_project, locales=self.db_project.locales.all())
     assert_false(has_changed)
Example #8
0
    def test_unsure_changes(self):
        """
        If any of the repos returns None as a revision number, consider
        the VCS as changed even if the revisions match the last sync.
        """
        self.mock_repo_pull.return_value = {'single_locale': None}
        self.repository.last_synced_revisions = {'single_locale': None}
        self.repository.save()

        assert_true(pull_changes(self.db_project))
Example #9
0
 def test_basic(self):
     """
     Pull_changes should call repo.pull for each repo for the
     project, save the return value to repo.last_synced_revisions,
     and return whether any changes happened in VCS.
     """
     self.mock_repo_pull.return_value = {'single_locale': 'asdf'}
     assert_true(pull_changes(self.db_project))
     self.repository.refresh_from_db()
     assert_equal(self.repository.last_synced_revisions, {'single_locale': 'asdf'})
Example #10
0
    def test_basic(self):
        """
        Pull_changes should call repo.pull for each repo for the
        project and return whether any changes happened in VCS.
        """
        mock_db_project = MagicMock()
        mock_db_project.repositories.all.return_value = [self.repository]
        self.mock_repo_pull.return_value = {'single_locale': 'asdf'}

        has_changed = pull_changes(self.db_project)
        assert_true(has_changed)
Example #11
0
    def test_basic(self):
        """
        Pull_changes should call repo.pull for each repo for the
        project and return whether any changes happened in VCS.
        """
        mock_db_project = MagicMock()
        mock_db_project.repositories.all.return_value = [self.repository]
        self.mock_repo_pull.return_value = {'single_locale': 'asdf'}

        has_changed, _ = pull_changes(self.db_project)
        assert_true(has_changed)
Example #12
0
def sync_project(self, project_pk, sync_log_pk, no_pull=False, no_commit=False, force=False):
    """Fetch the project with the given PK and perform sync on it."""
    db_project = get_or_fail(Project, pk=project_pk,
        message='Could not sync project with pk={0}, not found.'.format(project_pk))
    sync_log = get_or_fail(SyncLog, pk=sync_log_pk,
        message=('Could not sync project {0}, log with pk={1} not found.'
                 .format(db_project.slug, sync_log_pk)))

    log.info('Syncing project {0}.'.format(db_project.slug))

    # Mark "now" at the start of sync to avoid messing with
    # translations submitted during sync.
    now = timezone.now()

    project_sync_log = ProjectSyncLog.objects.create(
        sync_log=sync_log,
        project=db_project,
        start_time=now
    )

    if not no_pull:
        repos_changed = pull_changes(db_project)
    else:
        repos_changed = True  # Assume changed.

    # If the repos haven't changed since the last sync and there are
    # no Pontoon-side changes for this project, quit early.
    if not force and not repos_changed and not db_project.needs_sync:
        log.info('Skipping project {0}, no changes detected.'.format(db_project.slug))

        project_sync_log.skipped = True
        project_sync_log.skipped_end_time = timezone.now()
        project_sync_log.save(update_fields=('skipped', 'skipped_end_time'))

        return

    obsolete_vcs = perform_sync_project(db_project, now)

    for repo in db_project.repositories.all():
        sync_project_repo.delay(
            project_pk,
            repo.pk,
            project_sync_log.pk,
            now,
            obsolete_vcs,
            no_pull=no_pull,
            no_commit=no_commit
        )

    log.info('Synced resources for project {0}.'.format(db_project.slug))
Example #13
0
    def test_basic(self):
        """
        Pull_changes should call repo.pull for each repo for the
        project, save the return value to repo.last_synced_revisions,
        and return whether any changes happened in VCS.
        """
        mock_db_project = MagicMock()
        mock_db_project.repositories.all.return_value = [self.repository]
        self.mock_repo_pull.return_value = {'single_locale': 'asdf'}

        has_changed, revisions = pull_changes(self.db_project)
        assert_true(has_changed)
        assert_equal(revisions, {self.repository.pk: {'single_locale': 'asdf'}})
        self.repository.last_synced_revisions = revisions[self.repository.pk]
        self.repository.save()
        self.repository.refresh_from_db()
        assert_equal(self.repository.last_synced_revisions, {'single_locale': 'asdf'})
Example #14
0
def sync_sources(db_project, now, force, no_pull):
    # Pull source repository
    if no_pull:
        source_repo_changed = True  # Assume changed
    else:
        source_repo_changed, _ = pull_changes(db_project)

    # If the only repo hasn't changed since the last sync and there are
    # no Pontoon-side changes for this project, quit early.
    if (
        not force
        and not db_project.needs_sync
        and not source_repo_changed
        and db_project.has_single_repo
    ):
        log.info("Skipping project {0}, no changes detected.".format(db_project.slug))
        return False

    if force or source_repo_changed:
        try:
            added_paths, removed_paths, changed_paths, new_entities = update_originals(
                db_project, now, full_scan=force
            )
        except MissingSourceDirectoryError as e:
            log.error(e)
            return False

        if not db_project.has_single_repo:
            db_project.source_repository.set_last_synced_revisions()
        log.info("Synced sources for project {0}.".format(db_project.slug))

    else:
        added_paths, removed_paths, changed_paths, new_entities = None, None, None, None
        log.info(
            "Skipping syncing sources for project {0}, no changes detected.".format(
                db_project.slug
            )
        )

    return {
        "added_paths": added_paths,
        "removed_paths": removed_paths,
        "changed_paths": changed_paths,
        "new_entities": new_entities,
    }
Example #15
0
    def test_basic(self):
        """
        Pull_changes should call repo.pull for each repo for the
        project, save the return value to repo.last_synced_revisions,
        and return whether any changes happened in VCS.
        """
        mock_db_project = MagicMock()
        mock_db_project.repositories.all.return_value = [self.repository]
        self.mock_repo_pull.return_value = {'single_locale': 'asdf'}

        has_changed, revisions = pull_changes(self.db_project)
        assert_true(has_changed)
        assert_equal(revisions,
                     {self.repository.pk: {
                         'single_locale': 'asdf'
                     }})
        self.repository.last_synced_revisions = revisions[self.repository.pk]
        self.repository.save()
        self.repository.refresh_from_db()
        assert_equal(self.repository.last_synced_revisions,
                     {'single_locale': 'asdf'})
Example #16
0
def sync_project(self, project_pk, sync_log_pk, no_pull=False, no_commit=False, force=False):
    """Fetch the project with the given PK and perform sync on it."""
    db_project = get_or_fail(Project, pk=project_pk,
        message='Could not sync project with pk={0}, not found.'.format(project_pk))
    sync_log = get_or_fail(SyncLog, pk=sync_log_pk,
        message=('Could not sync project {0}, log with pk={1} not found.'
                 .format(db_project.slug, sync_log_pk)))

    log.info('Syncing project {0}.'.format(db_project.slug))

    # Mark "now" at the start of sync to avoid messing with
    # translations submitted during sync.
    now = timezone.now()

    project_sync_log = ProjectSyncLog.objects.create(
        sync_log=sync_log,
        project=db_project,
        start_time=now
    )

    if not no_pull:
        repos_changed = pull_changes(db_project)
    else:
        repos_changed = True  # Assume changed.

    # If the repos haven't changed since the last sync and there are
    # no Pontoon-side changes for this project, quit early.
    if not force and not repos_changed and not db_project.needs_sync:
        log.info('Skipping project {0}, no changes detected.'.format(db_project.slug))
        project_sync_log.skip()
        return

    try:
        obsolete_vcs_entities, obsolete_vcs_resources = perform_sync_project(db_project, now)
    except MissingSourceDirectoryError, e:
        log.error(e)
        project_sync_log.skip()
        return
Example #17
0
def sync_translations(
    self,
    project_pk,
    project_sync_log_pk,
    now,
    added_paths=None,
    removed_paths=None,
    changed_paths=None,
    new_entities=None,
    locale=None,
    no_pull=False,
    no_commit=False,
    full_scan=False,
):
    db_project = get_or_fail(
        Project,
        pk=project_pk,
        message="Could not sync project with pk={0}, not found.".format(project_pk),
    )

    repos = db_project.translation_repositories()
    repo_pk = repos[0].pk
    repo = get_or_fail(
        Repository,
        pk=repo_pk,
        message="Could not sync repo with pk={0}, not found.".format(repo_pk),
    )

    project_sync_log = get_or_fail(
        ProjectSyncLog,
        pk=project_sync_log_pk,
        message=(
            "Could not sync project {0}, log with pk={1} not found.".format(
                db_project.slug, project_sync_log_pk
            )
        ),
    )

    log.info("Syncing translations for project: {}".format(db_project.slug))

    repo_sync_log = RepositorySyncLog.objects.create(
        project_sync_log=project_sync_log, repository=repo, start_time=timezone.now()
    )

    if locale:
        locales = db_project.locales.filter(pk=locale.pk)
    else:
        locales = db_project.locales.all()

    if not locales:
        log.info(
            "Skipping syncing translations for project {0}, no locales to sync "
            "found within.".format(db_project.slug)
        )
        repo_sync_log.end()
        return

    # If project repositories have API access, we can retrieve latest commit hashes and detect
    # changed locales before the expensive VCS pull/clone operations. When performing full scan,
    # we still need to sync all locales.
    if not full_scan:
        locales = get_changed_locales(db_project, locales, now)

    readonly_locales = db_project.locales.filter(project_locale__readonly=True)
    added_and_changed_resources = db_project.resources.filter(
        path__in=list(added_paths or []) + list(changed_paths or [])
    ).distinct()

    # We should also sync files for which source file change - but only for read-only locales.
    # See bug 1372151 for more details.
    if added_and_changed_resources:
        changed_locales_pks = [l.pk for l in locales]
        readonly_locales_pks = [l.pk for l in readonly_locales]
        locales = db_project.locales.filter(
            pk__in=changed_locales_pks + readonly_locales_pks
        )

    # Pull VCS changes in case we're on a different worker than the one
    # sync started on.
    if not no_pull:
        log.info("Pulling changes for project {0} started.".format(db_project.slug))
        repos_changed, repo_locales = pull_changes(db_project, locales)
        repos = repos.filter(pk__in=repo_locales.keys())
        log.info("Pulling changes for project {0} complete.".format(db_project.slug))

    # If none of the repos has changed since the last sync and there are
    # no Pontoon-side changes for this project, quit early.
    if (
        not full_scan
        and not db_project.needs_sync
        and not repos_changed
        and not (added_paths or removed_paths or changed_paths)
    ):
        log.info("Skipping project {0}, no changes detected.".format(db_project.slug))
        repo_sync_log.end()
        return

    vcs_project = VCSProject(
        db_project,
        now,
        locales=locales,
        repo_locales=repo_locales,
        added_paths=added_paths,
        changed_paths=changed_paths,
        full_scan=full_scan,
    )

    synced_locales = set()
    failed_locales = set()

    # Store newly added locales and locales with newly added resources
    new_locales = []

    for locale in locales:
        try:
            with transaction.atomic():
                # Sets VCSProject.synced_locales, needed to skip early
                if not vcs_project.synced_locales:
                    vcs_project.resources

                # Skip all locales if none of the them has anything to sync
                if len(vcs_project.synced_locales) == 0:
                    break

                # Skip locales that have nothing to sync
                if (
                    vcs_project.synced_locales
                    and locale not in vcs_project.synced_locales
                ):
                    continue

                changeset = ChangeSet(db_project, vcs_project, now, locale)
                update_translations(db_project, vcs_project, locale, changeset)
                changeset.execute()

                created = update_translated_resources(db_project, vcs_project, locale)
                if created:
                    new_locales.append(locale.pk)
                update_locale_project_locale_stats(locale, db_project)

                # Clear out the "has_changed" markers now that we've finished
                # syncing.
                (
                    ChangedEntityLocale.objects.filter(
                        entity__resource__project=db_project,
                        locale=locale,
                        when__lte=now,
                    ).delete()
                )

                # Perform the commit last so that, if it succeeds, there is
                # nothing after it to fail.
                if (
                    not no_commit
                    and locale in changeset.locales_to_commit
                    and locale not in readonly_locales
                ):
                    commit_changes(db_project, vcs_project, changeset, locale)

                log.info(
                    "Synced locale {locale} for project {project}.".format(
                        locale=locale.code, project=db_project.slug,
                    )
                )

                synced_locales.add(locale.code)

        except CommitToRepositoryException as err:
            # Transaction aborted, log and move on to the next locale.
            log.warning(
                "Failed to sync locale {locale} for project {project} due to "
                "commit error: {error}".format(
                    locale=locale.code, project=db_project.slug, error=err,
                )
            )

            failed_locales.add(locale.code)

    # If sources have changed, update stats for all locales.
    if added_paths or removed_paths or changed_paths:
        for locale in db_project.locales.all():
            # Already synced.
            if locale.code in synced_locales:
                continue

            # We have files: update all translated resources.
            if locale in locales:
                created = update_translated_resources(db_project, vcs_project, locale)
                if created:
                    new_locales.append[locale.pk]

            # We don't have files: we can still update asymmetric translated resources.
            else:
                update_translated_resources_no_files(
                    db_project, locale, added_and_changed_resources,
                )

            update_locale_project_locale_stats(locale, db_project)
            synced_locales.add(locale.code)

            log.info(
                "Synced source changes for locale {locale} for project {project}.".format(
                    locale=locale.code, project=db_project.slug,
                )
            )

        db_project.aggregate_stats()

    if synced_locales:
        log.info(
            "Synced translations for project {0} in locales {1}.".format(
                db_project.slug, ",".join(synced_locales)
            )
        )
    elif failed_locales:
        log.info(
            "Failed to sync translations for project {0} due to commit error.".format(
                db_project.slug
            )
        )
    else:
        log.info(
            "Skipping syncing translations for project {0}, none of the locales "
            "has anything to sync.".format(db_project.slug)
        )

    for r in repos:
        r.set_last_synced_revisions(
            locales=repo_locales[r.pk].exclude(code__in=failed_locales)
        )
    repo_sync_log.end()

    if db_project.pretranslation_enabled:
        # Pretranslate all entities for newly added locales
        # and locales with newly added resources
        if len(new_locales):
            pretranslate(db_project, locales=new_locales)

        locales = db_project.locales.exclude(pk__in=new_locales).values_list(
            "pk", flat=True
        )

        # Pretranslate newly added entities for all locales
        if new_entities and locales:
            new_entities = list(set(new_entities))
            pretranslate(db_project, locales=locales, entities=new_entities)
Example #18
0
def sync_project(self,
                 project_pk,
                 sync_log_pk,
                 locale=None,
                 no_pull=False,
                 no_commit=False,
                 force=False):
    """Fetch the project with the given PK and perform sync on it."""
    db_project = get_or_fail(
        Project,
        pk=project_pk,
        message='Could not sync project with pk={0}, not found.'.format(
            project_pk))
    sync_log = get_or_fail(
        SyncLog,
        pk=sync_log_pk,
        message=(
            'Could not sync project {0}, log with pk={1} not found.'.format(
                db_project.slug, sync_log_pk)))

    log.info('Syncing project {0}.'.format(db_project.slug))

    # Mark "now" at the start of sync to avoid messing with
    # translations submitted during sync.
    now = timezone.now()

    project_sync_log = ProjectSyncLog.objects.create(sync_log=sync_log,
                                                     project=db_project,
                                                     start_time=now)

    # We have to cache changed files before last revision is overriden by pull
    if no_pull:
        repos_changed = True  # Assume changed.
    else:
        repos_changed, _ = pull_changes(db_project)

    # If the repos haven't changed since the last sync and there are
    # no Pontoon-side changes for this project, quit early.
    if not force and not repos_changed and not db_project.needs_sync:
        log.info('Skipping project {0}, no changes detected.'.format(
            db_project.slug))
        project_sync_log.skip()
        return

    # Do not sync resources if locale specified
    if locale:
        repo = locale.get_repository(db_project)
        if repo:
            sync_project_repo.delay(project_pk,
                                    repo.pk,
                                    project_sync_log.pk,
                                    now,
                                    locale=locale,
                                    no_pull=no_pull,
                                    no_commit=no_commit,
                                    full_scan=force)

    else:
        try:
            obsolete_vcs_entities, obsolete_vcs_resources, new_paths = perform_sync_project(
                db_project, now, full_scan=force)
        except MissingSourceDirectoryError, e:
            log.error(e)
            project_sync_log.skip()
            return

        log.info('Synced resources for project {0}.'.format(db_project.slug))

        # No need to sync translations if it's a source repository
        for repo in db_project.repositories.exclude(source_repo=True):
            sync_project_repo.delay(project_pk,
                                    repo.pk,
                                    project_sync_log.pk,
                                    now,
                                    obsolete_vcs_entities,
                                    obsolete_vcs_resources,
                                    new_paths,
                                    no_pull=no_pull,
                                    no_commit=no_commit,
                                    full_scan=force)

        for repo in db_project.repositories.filter(source_repo=True):
            repo.set_current_last_synced_revisions()
Example #19
0
def sync_project_repo(self,
                      project_pk,
                      repo_pk,
                      project_sync_log_pk,
                      now,
                      obsolete_vcs_entities=None,
                      obsolete_vcs_resources=None,
                      new_paths=None,
                      locale=None,
                      no_pull=False,
                      no_commit=False,
                      full_scan=False):
    db_project = get_or_fail(
        Project,
        pk=project_pk,
        message='Could not sync project with pk={0}, not found.'.format(
            project_pk))
    repo = get_or_fail(
        Repository,
        pk=repo_pk,
        message='Could not sync repo with pk={0}, not found.'.format(repo_pk))
    project_sync_log = get_or_fail(
        ProjectSyncLog,
        pk=project_sync_log_pk,
        message=(
            'Could not sync project {0}, log with pk={1} not found.'.format(
                db_project.slug, project_sync_log_pk)))
    log.info('Syncing repo: {}'.format(repo.url))

    repo_sync_log = RepositorySyncLog.objects.create(
        project_sync_log=project_sync_log,
        repository=repo,
        start_time=timezone.now())

    # Pull VCS changes in case we're on a different worker than the one
    # sync started on.
    if not no_pull:
        pull_changes(db_project)

    if locale:
        locales = [locale]
    else:
        locales = repo.locales

    # Cannot skip earlier - repo.locales is only available after pull_changes()
    if not locales:
        log.debug(
            'Skipping repo `{0}` for project {1}, no locales to sync found within.'
            .format(repo.url, db_project.slug))
        repo_sync_log.end()
        return

    vcs_project = VCSProject(
        db_project,
        locales=locales,
        obsolete_entities_paths=Resource.objects.obsolete_entities_paths(
            obsolete_vcs_entities),
        new_paths=new_paths,
        full_scan=full_scan)

    for locale in locales:
        try:
            with transaction.atomic():
                # Skip locales that have nothing to sync
                if vcs_project.synced_locales and locale not in vcs_project.synced_locales:
                    if obsolete_vcs_entities or obsolete_vcs_resources:
                        update_locale_project_locale_stats(locale, db_project)
                    continue

                changeset = ChangeSet(db_project, vcs_project, now,
                                      obsolete_vcs_entities,
                                      obsolete_vcs_resources)
                update_translations(db_project, vcs_project, locale, changeset)
                changeset.execute()

                update_translated_resources(db_project, vcs_project, changeset,
                                            locale)

                # Skip if none of the locales has anything to sync
                # VCSProject.synced_locales is set on a first call to
                # VCSProject.resources, which is set in
                # pontoon.sync.core.update_translated_resources()
                if len(vcs_project.synced_locales) == 0:
                    if obsolete_vcs_entities or obsolete_vcs_resources:
                        for l in locales:
                            update_locale_project_locale_stats(l, db_project)
                        db_project.aggregate_stats()

                    log.info(
                        'Skipping repo `{0}` for project {1}, none of the locales has anything to sync.'
                        .format(repo.url, db_project.slug))
                    repo_sync_log.end()
                    return

                update_locale_project_locale_stats(locale, db_project)

                # Clear out the "has_changed" markers now that we've finished
                # syncing.
                (ChangedEntityLocale.objects.filter(
                    entity__resource__project=db_project,
                    locale=locale,
                    when__lte=now).delete())

                # Clean up any duplicate approvals at the end of sync right
                # before we commit the transaction to avoid race conditions.
                with connection.cursor() as cursor:
                    cursor.execute(
                        """
                        UPDATE base_translation AS b
                        SET approved = FALSE, approved_date = NULL
                        WHERE
                          id IN
                            (SELECT trans.id FROM base_translation AS trans
                             LEFT JOIN base_entity AS ent ON ent.id = trans.entity_id
                             LEFT JOIN base_resource AS res ON res.id = ent.resource_id
                             WHERE locale_id = %(locale_id)s
                               AND res.project_id = %(project_id)s)
                          AND approved_date !=
                            (SELECT max(approved_date)
                             FROM base_translation
                             WHERE entity_id = b.entity_id
                               AND locale_id = b.locale_id
                               AND (plural_form = b.plural_form OR plural_form IS NULL));
                    """, {
                            'locale_id': locale.id,
                            'project_id': db_project.id
                        })

                # Perform the commit last so that, if it succeeds, there is
                # nothing after it to fail.
                if not no_commit and locale in changeset.locales_to_commit:
                    commit_changes(db_project, vcs_project, changeset, locale)

        except CommitToRepositoryException as err:
            # Transaction aborted, log and move on to the next locale.
            log.warning(
                'Failed to sync locale {locale} for project {project} due to '
                'commit error: {error}'.format(
                    locale=locale.code,
                    project=db_project.slug,
                    error=err,
                ))

    with transaction.atomic():
        db_project.aggregate_stats()

    log.info('Synced translations for project {0} in locales {1}.'.format(
        db_project.slug,
        ','.join(locale.code for locale in vcs_project.synced_locales)))
    repo_sync_log.end()
Example #20
0
def sync_translations(self,
                      project_pk,
                      project_sync_log_pk,
                      now,
                      project_changes=None,
                      obsolete_vcs_resources=None,
                      new_paths=None,
                      locale=None,
                      no_pull=False,
                      no_commit=False,
                      full_scan=False):
    db_project = get_or_fail(
        Project,
        pk=project_pk,
        message='Could not sync project with pk={0}, not found.'.format(
            project_pk))

    repos = db_project.translation_repositories()
    repo_pk = repos[0].pk
    repo = get_or_fail(
        Repository,
        pk=repo_pk,
        message='Could not sync repo with pk={0}, not found.'.format(repo_pk))

    project_sync_log = get_or_fail(
        ProjectSyncLog,
        pk=project_sync_log_pk,
        message=(
            'Could not sync project {0}, log with pk={1} not found.'.format(
                db_project.slug, project_sync_log_pk)))

    log.info('Syncing translations for project: {}'.format(db_project.slug))

    repo_sync_log = RepositorySyncLog.objects.create(
        project_sync_log=project_sync_log,
        repository=repo,
        start_time=timezone.now())

    if locale:
        locales = db_project.locales.filter(pk=locale.pk)
    else:
        locales = db_project.locales.all()

    if not locales:
        log.info(
            'Skipping syncing translations for project {0}, no locales to sync found within.'
            .format(db_project.slug))
        repo_sync_log.end()
        return

    # If project repositories have API access, we can retrieve latest commit hashes and detect
    # changed locales before the expensive VCS pull/clone operations. When performing full scan,
    # we still need to sync all locales.
    if not full_scan:
        locales = get_changed_locales(db_project, locales, now)

    # Pull VCS changes in case we're on a different worker than the one
    # sync started on.
    if not no_pull:
        log.info('Pulling changes for project {0} started.'.format(
            db_project.slug))
        repos_changed, repo_locales = pull_changes(db_project, locales)
        repos = repos.filter(pk__in=repo_locales.keys())
        log.info('Pulling changes for project {0} complete.'.format(
            db_project.slug))

    changed_resources = []
    obsolete_vcs_entities = []

    if project_changes:
        updated_entity_pks = []
        for locale_code, db_entity, vcs_entity in project_changes['update_db']:
            updated_entity_pks.append(db_entity.pk)

        obsolete_entity_pks = project_changes['obsolete_db']
        changed_resources = db_project.resources.filter(
            Q(entities__date_created=now)
            | Q(entities__pk__in=updated_entity_pks +
                obsolete_entity_pks)).distinct()

        obsolete_vcs_entities = project_changes['obsolete_db']

    # If none of the repos has changed since the last sync and there are
    # no Pontoon-side changes for this project, quit early.
    if (not full_scan and not db_project.needs_sync and not repos_changed
            and not (changed_resources or obsolete_vcs_resources)):
        log.info('Skipping project {0}, no changes detected.'.format(
            db_project.slug))
        repo_sync_log.end()
        return

    obsolete_entities_paths = (
        Resource.objects.obsolete_entities_paths(obsolete_vcs_entities)
        if obsolete_vcs_entities else None)

    vcs_project = VCSProject(db_project,
                             now,
                             locales=locales,
                             repo_locales=repo_locales,
                             obsolete_entities_paths=obsolete_entities_paths,
                             new_paths=new_paths,
                             full_scan=full_scan)

    synced_locales = set()
    failed_locales = set()

    for locale in locales:
        try:
            with transaction.atomic():
                # Sets VCSProject.synced_locales, needed to skip early
                if not vcs_project.synced_locales:
                    vcs_project.resources

                # Skip all locales if none of the them has anything to sync
                if len(vcs_project.synced_locales) == 0:
                    break

                # Skip locales that have nothing to sync
                if vcs_project.synced_locales and locale not in vcs_project.synced_locales:
                    continue

                changeset = ChangeSet(db_project, vcs_project, now, locale)
                update_translations(db_project, vcs_project, locale, changeset)
                changeset.execute()
                update_translated_resources(db_project, vcs_project, locale)
                update_locale_project_locale_stats(locale, db_project)

                # Clear out the "has_changed" markers now that we've finished
                # syncing.
                (ChangedEntityLocale.objects.filter(
                    entity__resource__project=db_project,
                    locale=locale,
                    when__lte=now).delete())

                # Clean up any duplicate approvals at the end of sync right
                # before we commit the transaction to avoid race conditions.
                with connection.cursor() as cursor:
                    cursor.execute(
                        """
                        UPDATE base_translation AS b
                        SET approved = FALSE, approved_date = NULL
                        WHERE
                          id IN
                            (SELECT trans.id FROM base_translation AS trans
                             LEFT JOIN base_entity AS ent ON ent.id = trans.entity_id
                             LEFT JOIN base_resource AS res ON res.id = ent.resource_id
                             WHERE locale_id = %(locale_id)s
                               AND res.project_id = %(project_id)s)
                          AND approved_date !=
                            (SELECT max(approved_date)
                             FROM base_translation
                             WHERE entity_id = b.entity_id
                               AND locale_id = b.locale_id
                               AND (plural_form = b.plural_form OR plural_form IS NULL));
                    """, {
                            'locale_id': locale.id,
                            'project_id': db_project.id
                        })

                # Perform the commit last so that, if it succeeds, there is
                # nothing after it to fail.
                if not no_commit and locale in changeset.locales_to_commit:
                    commit_changes(db_project, vcs_project, changeset, locale)

                log.info(
                    'Synced locale {locale} for project {project}.'.format(
                        locale=locale.code,
                        project=db_project.slug,
                    ))

                synced_locales.add(locale.code)

        except CommitToRepositoryException as err:
            # Transaction aborted, log and move on to the next locale.
            log.warning(
                'Failed to sync locale {locale} for project {project} due to '
                'commit error: {error}'.format(
                    locale=locale.code,
                    project=db_project.slug,
                    error=err,
                ))

            failed_locales.add(locale.code)

    # If sources have changed, update stats for all locales.
    if changed_resources or obsolete_vcs_resources:
        for locale in db_project.locales.all():
            # Already synced.
            if locale.code in synced_locales:
                continue

            # We have files: update all translated resources.
            if locale in locales:
                update_translated_resources(db_project, vcs_project, locale)

            # We don't have files: we can still update asymmetric translated resources.
            else:
                update_translated_resources_no_files(db_project, locale,
                                                     changed_resources)

            update_locale_project_locale_stats(locale, db_project)
            synced_locales.add(locale.code)

            log.info(
                'Synced source changes for locale {locale} for project {project}.'
                .format(
                    locale=locale.code,
                    project=db_project.slug,
                ))

        db_project.aggregate_stats()

    if synced_locales:
        log.info('Synced translations for project {0} in locales {1}.'.format(
            db_project.slug, ','.join(synced_locales)))
    elif failed_locales:
        log.info(
            'Failed to sync translations for project {0} due to commit error.'.
            format(db_project.slug))
    else:
        log.info(
            'Skipping syncing translations for project {0}, none of the locales '
            'has anything to sync.'.format(db_project.slug))

    for r in repos:
        r.set_last_synced_revisions(locales=repo_locales[r.pk].exclude(
            code__in=failed_locales))
    repo_sync_log.end()
Example #21
0
def sync_translations(self, project_pk, repo_pk, project_sync_log_pk, now, project_changes=None,
                      obsolete_vcs_resources=None, new_paths=None, locale=None, no_pull=False, no_commit=False,
                      full_scan=False):
    db_project = get_or_fail(Project, pk=project_pk,
        message='Could not sync project with pk={0}, not found.'.format(project_pk))
    repo = get_or_fail(Repository, pk=repo_pk,
        message='Could not sync repo with pk={0}, not found.'.format(repo_pk))
    project_sync_log = get_or_fail(ProjectSyncLog, pk=project_sync_log_pk,
        message=('Could not sync project {0}, log with pk={1} not found.'
                 .format(db_project.slug, project_sync_log_pk)))

    log.info('Syncing translations for repo: {}'.format(repo.url))

    repo_sync_log = RepositorySyncLog.objects.create(
        project_sync_log=project_sync_log,
        repository=repo,
        start_time=timezone.now()
    )

    if locale:
        locales = [locale]
    else:
        locales = repo.locales

    if not locales:
        log.info('Skipping repo `{0}` for project {1}, no locales to sync found within.'
                  .format(repo.url, db_project.slug))
        repo_sync_log.end()
        return

    # Pull VCS changes in case we're on a different worker than the one
    # sync started on.
    if not no_pull:
        repos_changed = pull_changes(db_project)

    resources_changed = []
    obsolete_vcs_entities = []
    if project_changes:
        resources_changed = (
            project_changes['update_db'] +
            project_changes['obsolete_db'] +
            project_changes['create_db']
        )
        obsolete_vcs_entities = project_changes['obsolete_db']

    # If none of the repos has changed since the last sync and there are
    # no Pontoon-side changes for this project, quit early.
    if not full_scan and not db_project.needs_sync and not repos_changed and not (resources_changed or obsolete_vcs_resources):
        log.info('Skipping project {0}, no changes detected.'.format(db_project.slug))
        repo_sync_log.end()
        return

    obsolete_entities_paths = Resource.objects.obsolete_entities_paths(obsolete_vcs_entities) if obsolete_vcs_entities else None

    vcs_project = VCSProject(
        db_project,
        locales=locales,
        obsolete_entities_paths=obsolete_entities_paths,
        new_paths=new_paths,
        full_scan=full_scan
    )

    failed_locales = set()

    for locale in locales:
        try:
            with transaction.atomic():
                # Sets VCSProject.synced_locales, needed to skip early
                if not vcs_project.synced_locales:
                    vcs_project.resources

                if not obsolete_vcs_resources:
                    # Skip all locales if none of the them has anything to sync
                    if len(vcs_project.synced_locales) == 0:
                        if resources_changed:
                            for l in locales:
                                update_translated_resources(db_project, vcs_project, l)
                                update_locale_project_locale_stats(l, db_project)
                            db_project.aggregate_stats()

                        log.info('Skipping repo `{0}` for project {1}, none of the locales has anything to sync.'
                                 .format(repo.url, db_project.slug))
                        repo.set_last_synced_revisions()
                        repo_sync_log.end()
                        return

                    # Skip locales that have nothing to sync
                    if vcs_project.synced_locales and locale not in vcs_project.synced_locales:
                        if resources_changed:
                            update_translated_resources(db_project, vcs_project, locale)
                            update_locale_project_locale_stats(locale, db_project)
                            log.debug('Skipping locale `{0}` for project {1}, no changes detected.'
                                      .format(locale.code, db_project.slug))
                        continue

                changeset = ChangeSet(db_project, vcs_project, now, obsolete_vcs_entities, obsolete_vcs_resources, locale)
                update_translations(db_project, vcs_project, locale, changeset)
                changeset.execute()
                update_translated_resources(db_project, vcs_project, locale)
                update_locale_project_locale_stats(locale, db_project)

                # Clear out the "has_changed" markers now that we've finished
                # syncing.
                (ChangedEntityLocale.objects
                    .filter(entity__resource__project=db_project,
                            locale=locale,
                            when__lte=now)
                    .delete())

                # Clean up any duplicate approvals at the end of sync right
                # before we commit the transaction to avoid race conditions.
                with connection.cursor() as cursor:
                    cursor.execute("""
                        UPDATE base_translation AS b
                        SET approved = FALSE, approved_date = NULL
                        WHERE
                          id IN
                            (SELECT trans.id FROM base_translation AS trans
                             LEFT JOIN base_entity AS ent ON ent.id = trans.entity_id
                             LEFT JOIN base_resource AS res ON res.id = ent.resource_id
                             WHERE locale_id = %(locale_id)s
                               AND res.project_id = %(project_id)s)
                          AND approved_date !=
                            (SELECT max(approved_date)
                             FROM base_translation
                             WHERE entity_id = b.entity_id
                               AND locale_id = b.locale_id
                               AND (plural_form = b.plural_form OR plural_form IS NULL));
                    """, {
                        'locale_id': locale.id,
                        'project_id': db_project.id
                    })

                # Perform the commit last so that, if it succeeds, there is
                # nothing after it to fail.
                if not no_commit and locale in changeset.locales_to_commit:
                    commit_changes(db_project, vcs_project, changeset, locale)

                log.info(
                    'Synced locale {locale} for project {project}.'.format(
                        locale=locale.code,
                        project=db_project.slug,
                    )
                )

        except CommitToRepositoryException as err:
            # Transaction aborted, log and move on to the next locale.
            log.warning(
                'Failed to sync locale {locale} for project {project} due to '
                'commit error: {error}'.format(
                    locale=locale.code,
                    project=db_project.slug,
                    error=err,
                )
            )

            failed_locales.add(locale)

    with transaction.atomic():
        db_project.aggregate_stats()

    synced_locales = [locale.code for locale in (vcs_project.synced_locales - failed_locales)]

    if synced_locales:
        log.info('Synced translations for project {0} in locales {1}.'.format(
            db_project.slug, ','.join(synced_locales)
        ))
    else:
        log.info('Failed to sync translations for project {0} due to commit error.'.format(
            db_project.slug
        ))

    repo.set_last_synced_revisions(exclude=failed_locales)
    repo_sync_log.end()
Example #22
0
def sync_translations(
    self, project_pk, project_sync_log_pk, now, added_paths=None, removed_paths=None,
    changed_paths=None, locale=None, no_pull=False, no_commit=False, full_scan=False,
):
    db_project = get_or_fail(
        Project,
        pk=project_pk,
        message='Could not sync project with pk={0}, not found.'.format(project_pk)
    )

    repos = db_project.translation_repositories()
    repo_pk = repos[0].pk
    repo = get_or_fail(
        Repository,
        pk=repo_pk,
        message='Could not sync repo with pk={0}, not found.'.format(repo_pk)
    )

    project_sync_log = get_or_fail(
        ProjectSyncLog,
        pk=project_sync_log_pk,
        message=(
            'Could not sync project {0}, log with pk={1} not found.'
            .format(db_project.slug, project_sync_log_pk)
        )
    )

    log.info('Syncing translations for project: {}'.format(db_project.slug))

    repo_sync_log = RepositorySyncLog.objects.create(
        project_sync_log=project_sync_log,
        repository=repo,
        start_time=timezone.now()
    )

    if locale:
        locales = db_project.locales.filter(pk=locale.pk)
    else:
        locales = db_project.locales.all()

    if not locales:
        log.info('Skipping syncing translations for project {0}, no locales to sync found within.'
                 .format(db_project.slug))
        repo_sync_log.end()
        return

    # If project repositories have API access, we can retrieve latest commit hashes and detect
    # changed locales before the expensive VCS pull/clone operations. When performing full scan,
    # we still need to sync all locales.
    if not full_scan:
        locales = get_changed_locales(db_project, locales, now)

    readonly_locales = db_project.locales.filter(project_locale__readonly=True)
    added_and_changed_resources = db_project.resources.filter(
        path__in=list(added_paths or []) + list(changed_paths or [])
    ).distinct()

    # We should also sync files for which source file change - but only for read-only locales.
    # See bug 1372151 for more details.
    if added_and_changed_resources:
        changed_locales_pks = [l.pk for l in locales]
        readonly_locales_pks = [l.pk for l in readonly_locales]
        locales = db_project.locales.filter(
            pk__in=changed_locales_pks + readonly_locales_pks
        )

    # Pull VCS changes in case we're on a different worker than the one
    # sync started on.
    if not no_pull:
        log.info('Pulling changes for project {0} started.'.format(db_project.slug))
        repos_changed, repo_locales = pull_changes(db_project, locales)
        repos = repos.filter(pk__in=repo_locales.keys())
        log.info('Pulling changes for project {0} complete.'.format(db_project.slug))

    # If none of the repos has changed since the last sync and there are
    # no Pontoon-side changes for this project, quit early.
    if (
        not full_scan and
        not db_project.needs_sync and
        not repos_changed and
        not (added_paths or removed_paths or changed_paths)
    ):
        log.info('Skipping project {0}, no changes detected.'.format(db_project.slug))
        repo_sync_log.end()
        return

    vcs_project = VCSProject(
        db_project,
        now,
        locales=locales,
        repo_locales=repo_locales,
        added_paths=added_paths,
        changed_paths=changed_paths,
        full_scan=full_scan
    )

    synced_locales = set()
    failed_locales = set()

    for locale in locales:
        try:
            with transaction.atomic():
                # Sets VCSProject.synced_locales, needed to skip early
                if not vcs_project.synced_locales:
                    vcs_project.resources

                # Skip all locales if none of the them has anything to sync
                if len(vcs_project.synced_locales) == 0:
                    break

                # Skip locales that have nothing to sync
                if vcs_project.synced_locales and locale not in vcs_project.synced_locales:
                    continue

                changeset = ChangeSet(db_project, vcs_project, now, locale)
                update_translations(db_project, vcs_project, locale, changeset)
                changeset.execute()
                update_translated_resources(db_project, vcs_project, locale)
                update_locale_project_locale_stats(locale, db_project)

                # Clear out the "has_changed" markers now that we've finished
                # syncing.
                (ChangedEntityLocale.objects
                    .filter(entity__resource__project=db_project,
                            locale=locale,
                            when__lte=now)
                    .delete())

                # Perform the commit last so that, if it succeeds, there is
                # nothing after it to fail.
                if (
                    not no_commit and
                    locale in changeset.locales_to_commit and
                    locale not in readonly_locales
                ):
                    commit_changes(db_project, vcs_project, changeset, locale)

                log.info(
                    'Synced locale {locale} for project {project}.'.format(
                        locale=locale.code,
                        project=db_project.slug,
                    )
                )

                synced_locales.add(locale.code)

        except CommitToRepositoryException as err:
            # Transaction aborted, log and move on to the next locale.
            log.warning(
                'Failed to sync locale {locale} for project {project} due to '
                'commit error: {error}'.format(
                    locale=locale.code,
                    project=db_project.slug,
                    error=err,
                )
            )

            failed_locales.add(locale.code)

    # If sources have changed, update stats for all locales.
    if added_paths or removed_paths or changed_paths:
        for locale in db_project.locales.all():
            # Already synced.
            if locale.code in synced_locales:
                continue

            # We have files: update all translated resources.
            if locale in locales:
                update_translated_resources(db_project, vcs_project, locale)

            # We don't have files: we can still update asymmetric translated resources.
            else:
                update_translated_resources_no_files(
                    db_project,
                    locale,
                    added_and_changed_resources,
                )

            update_locale_project_locale_stats(locale, db_project)
            synced_locales.add(locale.code)

            log.info(
                'Synced source changes for locale {locale} for project {project}.'.format(
                    locale=locale.code,
                    project=db_project.slug,
                )
            )

        db_project.aggregate_stats()

    if synced_locales:
        log.info('Synced translations for project {0} in locales {1}.'.format(
            db_project.slug, ','.join(synced_locales)
        ))
    elif failed_locales:
        log.info('Failed to sync translations for project {0} due to commit error.'.format(
            db_project.slug
        ))
    else:
        log.info(
            'Skipping syncing translations for project {0}, none of the locales '
            'has anything to sync.'.format(db_project.slug)
        )

    for r in repos:
        r.set_last_synced_revisions(
            locales=repo_locales[r.pk].exclude(code__in=failed_locales)
        )
    repo_sync_log.end()
Example #23
0
def sync_project_repo(self, project_pk, repo_pk, project_sync_log_pk, now, obsolete_vcs_entities=None,
                      obsolete_vcs_resources=None, locale=None, no_pull=False, no_commit=False,
                      full_scan=False):
    db_project = get_or_fail(Project, pk=project_pk,
        message='Could not sync project with pk={0}, not found.'.format(project_pk))
    repo = get_or_fail(Repository, pk=repo_pk,
        message='Could not sync repo with pk={0}, not found.'.format(repo_pk))
    project_sync_log = get_or_fail(ProjectSyncLog, pk=project_sync_log_pk,
        message=('Could not sync project {0}, log with pk={1} not found.'
                 .format(db_project.slug, project_sync_log_pk)))
    log.info('Syncing repo: {}'.format(repo.url))

    repo_sync_log = RepositorySyncLog.objects.create(
        project_sync_log=project_sync_log,
        repository=repo,
        start_time=timezone.now()
    )

    # Pull VCS changes in case we're on a different worker than the one
    # sync started on.
    if not no_pull:
        pull_changes(db_project)

    if locale:
        locales = [locale]
    else:
        locales = repo.locales

    vcs_project = VCSProject(
        db_project,
        locales=locales,
        obsolete_entities_paths=Resource.objects.obsolete_entities_paths(obsolete_vcs_entities),
        full_scan=full_scan
    )

    for locale in locales:
        try:
            with transaction.atomic():
                # Skip locales that have nothing to sync
                if vcs_project.synced_locales and locale not in vcs_project.synced_locales:
                    continue

                changeset = ChangeSet(db_project, vcs_project, now, obsolete_vcs_entities, obsolete_vcs_resources)
                update_translations(db_project, vcs_project, locale, changeset)
                changeset.execute()

                update_translated_resources(db_project, vcs_project, changeset, locale)

                # Skip if none of the locales has anything to sync
                # VCSProject.synced_locales is set on a first call to
                # VCSProject.resources, which is set in
                # pontoon.sync.core.update_translated_resources()
                if len(vcs_project.synced_locales) == 0:
                    log.info('Skipping repo `{0}` for project {1}, none of the locales has anything to sync.'
                             .format(repo.url, db_project.slug))
                    end_repo_sync(repo, repo_sync_log)
                    return

                locale.aggregate_stats()
                locale.project_locale.get(project=db_project).aggregate_stats()

                # Clear out the "has_changed" markers now that we've finished
                # syncing.
                (ChangedEntityLocale.objects
                    .filter(entity__resource__project=db_project,
                            locale=locale,
                            when__lte=now)
                    .delete())
                db_project.has_changed = False
                db_project.save(update_fields=['has_changed'])

                # Clean up any duplicate approvals at the end of sync right
                # before we commit the transaction to avoid race conditions.
                with connection.cursor() as cursor:
                    cursor.execute("""
                        UPDATE base_translation AS b
                        SET approved = FALSE, approved_date = NULL
                        WHERE
                          id IN
                            (SELECT trans.id FROM base_translation AS trans
                             LEFT JOIN base_entity AS ent ON ent.id = trans.entity_id
                             LEFT JOIN base_resource AS res ON res.id = ent.resource_id
                             WHERE locale_id = %(locale_id)s
                               AND res.project_id = %(project_id)s)
                          AND approved_date !=
                            (SELECT max(approved_date)
                             FROM base_translation
                             WHERE entity_id = b.entity_id
                               AND locale_id = b.locale_id
                               AND (plural_form = b.plural_form OR plural_form IS NULL));
                    """, {
                        'locale_id': locale.id,
                        'project_id': db_project.id
                    })

                # Perform the commit last so that, if it succeeds, there is
                # nothing after it to fail.
                if not no_commit and locale in changeset.locales_to_commit:
                    commit_changes(db_project, vcs_project, changeset, locale)

        except CommitToRepositoryException as err:
            # Transaction aborted, log and move on to the next locale.
            log.warning(
                'Failed to sync locale {locale} for project {project} due to '
                'commit error: {error}'.format(
                    locale=locale.code,
                    project=db_project.slug,
                    error=err,
                )
            )

    with transaction.atomic():
        db_project.aggregate_stats()

    log.info('Synced translations for project {0} in locales {1}.'.format(
        db_project.slug, ','.join(locale.code for locale in vcs_project.synced_locales)
    ))
    end_repo_sync(repo, repo_sync_log)
Example #24
0
def sync_project_repo(self, project_pk, repo_pk, project_sync_log_pk, now,
                      no_pull=False, no_commit=False):
    db_project = get_or_fail(Project, pk=project_pk,
        message='Could not sync project with pk={0}, not found.'.format(project_pk))
    repo = get_or_fail(Repository, pk=repo_pk,
        message='Could not sync repo with pk={0}, not found.'.format(project_pk))
    project_sync_log = get_or_fail(ProjectSyncLog, pk=project_sync_log_pk,
        message=('Could not sync project {0}, log with pk={1} not found.'
                 .format(db_project.slug, project_sync_log_pk)))

    repo_sync_log = RepositorySyncLog.objects.create(
        project_sync_log=project_sync_log,
        repository=repo,
        start_time=timezone.now()
    )

    # Pull VCS changes in case we're on a different worker than the one
    # sync started on.
    if not no_pull:
        pull_changes(db_project)

    if len(repo.locales) < 1:
        log.warning('Could not sync repo `{0}`, no locales found within.'
                    .format(repo.url))
        repo_sync_log.end_time = timezone.now()
        repo_sync_log.save(update_fields=['end_time'])
        return

    vcs_project = VCSProject(db_project, locales=repo.locales)
    for locale in repo.locales:
        try:
            with transaction.atomic():
                changeset = ChangeSet(db_project, vcs_project, now)
                update_translations(db_project, vcs_project, locale, changeset)
                changeset.execute()

                update_project_stats(db_project, vcs_project, changeset, locale)

                # Clear out the "has_changed" markers now that we've finished
                # syncing.
                (ChangedEntityLocale.objects
                    .filter(entity__resource__project=db_project,
                            locale=locale,
                            when__lte=now)
                    .delete())
                db_project.has_changed = False
                db_project.save(update_fields=['has_changed'])

                # Clean up any duplicate approvals at the end of sync right
                # before we commit the transaction to avoid race conditions.
                with connection.cursor() as cursor:
                    cursor.execute("""
                        UPDATE base_translation AS b
                        SET approved = FALSE, approved_date = NULL
                        WHERE
                          id IN
                            (SELECT trans.id FROM base_translation AS trans
                             LEFT JOIN base_entity AS ent ON ent.id = trans.entity_id
                             LEFT JOIN base_resource AS res ON res.id = ent.resource_id
                             WHERE locale_id = %(locale_id)s
                               AND res.project_id = %(project_id)s)
                          AND approved_date !=
                            (SELECT max(approved_date)
                             FROM base_translation
                             WHERE entity_id = b.entity_id
                               AND locale_id = b.locale_id
                               AND (plural_form = b.plural_form OR plural_form IS NULL));
                    """, {
                        'locale_id': locale.id,
                        'project_id': db_project.id
                    })

                # Perform the commit last so that, if it succeeds, there is
                # nothing after it to fail.
                if not no_commit and locale in changeset.locales_to_commit:
                    commit_changes(db_project, vcs_project, changeset, locale)
        except CommitToRepositoryException as err:
            # Transaction aborted, log and move on to the next locale.
            log.warning(
                'Failed to sync locale {locale} for project {project} due to '
                'commit error: {error}'.format(
                    locale=locale.code,
                    project=db_project.slug,
                    error=err,
                )
            )

    repo_sync_log.end_time = timezone.now()
    repo_sync_log.save()
    log.info('Synced translations for project {0} in locales {1}.'.format(
        db_project.slug, ','.join(locale.code for locale in repo.locales)
    ))
Example #25
0
def sync_project(self, project_pk, sync_log_pk, locale=None, no_pull=False, no_commit=False, force=False):
    """Fetch the project with the given PK and perform sync on it."""
    db_project = get_or_fail(Project, pk=project_pk,
        message='Could not sync project with pk={0}, not found.'.format(project_pk))
    sync_log = get_or_fail(SyncLog, pk=sync_log_pk,
        message=('Could not sync project {0}, log with pk={1} not found.'
                 .format(db_project.slug, sync_log_pk)))

    log.info('Syncing project {0}.'.format(db_project.slug))

    # Mark "now" at the start of sync to avoid messing with
    # translations submitted during sync.
    now = timezone.now()

    project_sync_log = ProjectSyncLog.objects.create(
        sync_log=sync_log,
        project=db_project,
        start_time=now
    )

    # We have to cache changed files before last revision is overriden by pull
    if no_pull:
        repos_changed = True  # Assume changed.
    else:
        repos_changed, _ = pull_changes(db_project)

    # If the repos haven't changed since the last sync and there are
    # no Pontoon-side changes for this project, quit early.
    if not force and not repos_changed and not db_project.needs_sync:
        log.info('Skipping project {0}, no changes detected.'.format(db_project.slug))
        project_sync_log.skip()
        return

    # Do not sync resources if locale specified
    if locale:
        repo = locale.get_repository(db_project)
        if repo:
            sync_project_repo.delay(
                project_pk,
                repo.pk,
                project_sync_log.pk,
                now,
                locale=locale,
                no_pull=no_pull,
                no_commit=no_commit,
                full_scan=force
            )

    else:
        try:
            obsolete_vcs_entities, obsolete_vcs_resources = perform_sync_project(db_project, now, full_scan=force)
        except MissingSourceDirectoryError, e:
            log.error(e)
            project_sync_log.skip()
            return

        log.info('Synced resources for project {0}.'.format(db_project.slug))

        # No need to sync translations if it's a source repository
        for repo in db_project.repositories.exclude(source_repo=True):
            sync_project_repo.delay(
                project_pk,
                repo.pk,
                project_sync_log.pk,
                now,
                obsolete_vcs_entities,
                obsolete_vcs_resources,
                no_pull=no_pull,
                no_commit=no_commit,
                full_scan=force
            )

        for repo in db_project.repositories.filter(source_repo=True):
            repo.set_current_last_synced_revisions()
Example #26
0
def sync_translations(
    self, project_pk, project_sync_log_pk, now, project_changes=None, obsolete_vcs_resources=None,
    new_paths=None, locale=None, no_pull=False, no_commit=False, full_scan=False
):
    db_project = get_or_fail(
        Project,
        pk=project_pk,
        message='Could not sync project with pk={0}, not found.'.format(project_pk)
    )

    repos = db_project.translation_repositories()
    repo_pk = repos[0].pk
    repo = get_or_fail(
        Repository,
        pk=repo_pk,
        message='Could not sync repo with pk={0}, not found.'.format(repo_pk)
    )

    project_sync_log = get_or_fail(
        ProjectSyncLog,
        pk=project_sync_log_pk,
        message=(
            'Could not sync project {0}, log with pk={1} not found.'
            .format(db_project.slug, project_sync_log_pk)
        )
    )

    log.info('Syncing translations for project: {}'.format(db_project.slug))

    repo_sync_log = RepositorySyncLog.objects.create(
        project_sync_log=project_sync_log,
        repository=repo,
        start_time=timezone.now()
    )

    if locale:
        locales = db_project.locales.filter(pk=locale.pk)
    else:
        locales = db_project.locales.all()

    if not locales:
        log.info('Skipping syncing translations for project {0}, no locales to sync found within.'
                 .format(db_project.slug))
        repo_sync_log.end()
        return

    # If project repositories have API access, we can retrieve latest commit hashes and detect
    # changed locales before the expensive VCS pull/clone operations. When performing full scan,
    # we still need to sync all locales.
    if not full_scan:
        locales = get_changed_locales(db_project, locales, now)

    # Pull VCS changes in case we're on a different worker than the one
    # sync started on.
    if not no_pull:
        log.info('Pulling changes for project {0} started.'.format(db_project.slug))
        repos_changed, repo_locales = pull_changes(db_project, locales)
        repos = repos.filter(pk__in=repo_locales.keys())
        log.info('Pulling changes for project {0} complete.'.format(db_project.slug))

    changed_resources = []
    obsolete_vcs_entities = []

    if project_changes:
        updated_entity_pks = []
        for locale_code, db_entity, vcs_entity in project_changes['update_db']:
            updated_entity_pks.append(db_entity.pk)

        obsolete_entity_pks = project_changes['obsolete_db']
        changed_resources = db_project.resources.filter(
            Q(entities__date_created=now) |
            Q(entities__pk__in=updated_entity_pks + obsolete_entity_pks)
        ).distinct()

        obsolete_vcs_entities = project_changes['obsolete_db']

    # If none of the repos has changed since the last sync and there are
    # no Pontoon-side changes for this project, quit early.
    if (
        not full_scan and
        not db_project.needs_sync and
        not repos_changed and
        not (changed_resources or obsolete_vcs_resources)
    ):
        log.info('Skipping project {0}, no changes detected.'.format(db_project.slug))
        repo_sync_log.end()
        return

    obsolete_entities_paths = (
        Resource.objects.obsolete_entities_paths(obsolete_vcs_entities) if obsolete_vcs_entities
        else None
    )

    vcs_project = VCSProject(
        db_project,
        now,
        locales=locales,
        repo_locales=repo_locales,
        obsolete_entities_paths=obsolete_entities_paths,
        new_paths=new_paths,
        full_scan=full_scan
    )

    synced_locales = set()
    failed_locales = set()

    for locale in locales:
        try:
            with transaction.atomic():
                # Sets VCSProject.synced_locales, needed to skip early
                if not vcs_project.synced_locales:
                    vcs_project.resources

                # Skip all locales if none of the them has anything to sync
                if len(vcs_project.synced_locales) == 0:
                    break

                # Skip locales that have nothing to sync
                if vcs_project.synced_locales and locale not in vcs_project.synced_locales:
                    continue

                changeset = ChangeSet(db_project, vcs_project, now, locale)
                update_translations(db_project, vcs_project, locale, changeset)
                changeset.execute()
                update_translated_resources(db_project, vcs_project, locale)
                update_locale_project_locale_stats(locale, db_project)

                # Clear out the "has_changed" markers now that we've finished
                # syncing.
                (ChangedEntityLocale.objects
                    .filter(entity__resource__project=db_project,
                            locale=locale,
                            when__lte=now)
                    .delete())

                # Clean up any duplicate approvals at the end of sync right
                # before we commit the transaction to avoid race conditions.
                with connection.cursor() as cursor:
                    cursor.execute("""
                        UPDATE base_translation AS b
                        SET approved = FALSE, approved_date = NULL
                        WHERE
                          id IN
                            (SELECT trans.id FROM base_translation AS trans
                             LEFT JOIN base_entity AS ent ON ent.id = trans.entity_id
                             LEFT JOIN base_resource AS res ON res.id = ent.resource_id
                             WHERE locale_id = %(locale_id)s
                               AND res.project_id = %(project_id)s)
                          AND approved_date !=
                            (SELECT max(approved_date)
                             FROM base_translation
                             WHERE entity_id = b.entity_id
                               AND locale_id = b.locale_id
                               AND (plural_form = b.plural_form OR plural_form IS NULL));
                    """, {
                        'locale_id': locale.id,
                        'project_id': db_project.id
                    })

                # Perform the commit last so that, if it succeeds, there is
                # nothing after it to fail.
                if not no_commit and locale in changeset.locales_to_commit:
                    commit_changes(db_project, vcs_project, changeset, locale)

                log.info(
                    'Synced locale {locale} for project {project}.'.format(
                        locale=locale.code,
                        project=db_project.slug,
                    )
                )

                synced_locales.add(locale.code)

        except CommitToRepositoryException as err:
            # Transaction aborted, log and move on to the next locale.
            log.warning(
                'Failed to sync locale {locale} for project {project} due to '
                'commit error: {error}'.format(
                    locale=locale.code,
                    project=db_project.slug,
                    error=err,
                )
            )

            failed_locales.add(locale.code)

    # If sources have changed, update stats for all locales.
    if changed_resources or obsolete_vcs_resources:
        for locale in db_project.locales.all():
            # Already synced.
            if locale.code in synced_locales:
                continue

            # We have files: update all translated resources.
            if locale in locales:
                update_translated_resources(db_project, vcs_project, locale)

            # We don't have files: we can still update asymmetric translated resources.
            else:
                update_translated_resources_no_files(db_project, locale, changed_resources)

            update_locale_project_locale_stats(locale, db_project)
            synced_locales.add(locale.code)

            log.info(
                'Synced source changes for locale {locale} for project {project}.'.format(
                    locale=locale.code,
                    project=db_project.slug,
                )
            )

        db_project.aggregate_stats()

    if synced_locales:
        log.info('Synced translations for project {0} in locales {1}.'.format(
            db_project.slug, ','.join(synced_locales)
        ))
    elif failed_locales:
        log.info('Failed to sync translations for project {0} due to commit error.'.format(
            db_project.slug
        ))
    else:
        log.info(
            'Skipping syncing translations for project {0}, none of the locales '
            'has anything to sync.'.format(db_project.slug)
        )

    for r in repos:
        r.set_last_synced_revisions(
            locales=repo_locales[r.pk].exclude(code__in=failed_locales)
        )
    repo_sync_log.end()