Example #1
0
 def clean_entity_ids(self):
     return utils.split_ints(self.cleaned_data["entity_ids"])
Example #2
0
def ajax_notifications(request, slug):
    """Notifications tab."""
    project = get_object_or_404(Project.objects.visible_for(
        request.user).available(),
                                slug=slug)
    available_locales = project.locales.order_by("name")

    # Send notifications
    if request.method == "POST":
        form = forms.NotificationsForm(request.POST)

        if not form.is_valid():
            return JsonResponse(dict(form.errors.items()))

        contributors = User.objects.filter(
            translation__entity__resource__project=project, )

        # For performance reasons, only filter contributors for selected
        # locales if different from all project locales
        available_ids = sorted(
            list(available_locales.values_list("id", flat=True)))
        selected_ids = sorted(
            split_ints(form.cleaned_data.get("selected_locales")))

        if available_ids != selected_ids:
            contributors = User.objects.filter(
                translation__entity__resource__project=project,
                translation__locale__in=available_locales.filter(
                    id__in=selected_ids),
            )

        identifier = uuid.uuid4().hex
        for contributor in contributors.distinct():
            notify.send(
                request.user,
                recipient=contributor,
                verb="has sent a message in",
                target=project,
                description=form.cleaned_data.get("message"),
                identifier=identifier,
            )

    # Detect previously sent notifications using a unique identifier
    # TODO: We should simplify this with a custom Notifications model
    notifications = []

    identifiers = {
        data["identifier"]
        for data in list(
            Notification.objects.filter(
                description__isnull=False,
                target_content_type=ContentType.objects.get_for_model(project),
                target_object_id=project.id,
            ).values_list("data", flat=True))
    }

    for identifier in identifiers:
        notifications.append(
            Notification.objects.filter(data__contains=identifier)[0])

    notifications.sort(key=lambda x: x.timestamp, reverse=True)

    # Recipient shortcuts
    incomplete = []
    complete = []
    for available_locale in available_locales:
        completion_percent = available_locale.get_chart(
            project)["completion_percent"]
        if completion_percent == 100:
            complete.append(available_locale.pk)
        else:
            incomplete.append(available_locale.pk)

    return render(
        request,
        "projects/includes/manual_notifications.html",
        {
            "form": forms.NotificationsForm(),
            "project": project,
            "available_locales": available_locales,
            "notifications": notifications,
            "incomplete": incomplete,
            "complete": complete,
        },
    )
Example #3
0
 def clean_exclude_entities(self):
     return utils.split_ints(self.cleaned_data["exclude_entities"])
Example #4
0
def entities(request):
    """Get entities for the specified project, locale and paths."""
    try:
        project = request.POST['project']
        locale = request.POST['locale']
        paths = request.POST.getlist('paths[]')
        limit = int(request.POST.get('limit', 50))
    except (MultiValueDictKeyError, ValueError) as err:
        return HttpResponseBadRequest('Bad Request: {error}'.format(error=err))

    project = get_object_or_404(Project, slug=project)
    locale = get_object_or_404(Locale, code=locale)

    status = request.POST.get('status', '')
    extra = request.POST.get('extra', '')
    time = request.POST.get('time', '')
    author = request.POST.get('author', '')
    search = request.POST.get('search', '')
    exclude_entities = split_ints(request.POST.get('excludeEntities', ''))

    # Only return entities with provided IDs (batch editing)
    entity_ids = split_ints(request.POST.get('entityIds', ''))

    if entity_ids:
        entities = (Entity.objects.filter(
            pk__in=entity_ids).prefetch_resources_translations(
                locale).distinct().order_by('order'))

        return JsonResponse(
            {
                'entities': Entity.map_entities(locale, entities),
                'stats': TranslatedResource.objects.stats(
                    project, paths, locale),
            },
            safe=False)

    entities = Entity.for_project_locale(project, locale, paths, status,
                                         search, exclude_entities, extra, time,
                                         author)

    # Only return a list of entity PKs (batch editing: select all)
    if request.POST.get('pkOnly', None):
        return JsonResponse({
            'entity_pks':
            list(entities.values_list('pk', flat=True)),
        })

    visible_entities = []

    # In-place view: load all entities
    if request.POST.get('inplaceEditor', None):
        has_next = False
        entities_to_map = Entity.for_project_locale(project, locale, paths,
                                                    None, None,
                                                    exclude_entities)
        visible_entities = entities.values_list('pk', flat=True)

    # Out-of-context view: paginate entities
    else:
        paginator = Paginator(entities, limit)

        try:
            entities_page = paginator.page(1)
        except EmptyPage:
            return JsonResponse({
                'has_next': False,
                'stats': {},
                'authors': [],
                'counts_per_minute': [],
            })

        has_next = entities_page.has_next()
        entities_to_map = entities_page.object_list

        # If requested entity not on the first page
        entity = request.POST.get('entity', None)
        if entity:
            try:
                entity_pk = int(entity)
            except ValueError as err:
                return HttpResponseBadRequest(
                    'Bad Request: {error}'.format(error=err))

            # TODO: entities_to_map.values_list() doesn't return entities from selected page
            if entity_pk not in [e.pk for e in entities_to_map]:
                if entity_pk in entities.values_list('pk', flat=True):
                    entities_to_map = list(entities_to_map) + list(
                        entities.filter(pk=entity_pk))

    return JsonResponse(
        {
            'entities':
            Entity.map_entities(locale, entities_to_map, visible_entities),
            'has_next':
            has_next,
            'stats':
            TranslatedResource.objects.stats(project, paths, locale),
        },
        safe=False)
Example #5
0
def batch_edit_translations(request):
    try:
        l = request.POST['locale']
        action = request.POST['action']
        entity_pks = split_ints(request.POST.get('entities', ''))
    except MultiValueDictKeyError as e:
        return HttpResponseBadRequest('Bad Request: {error}'.format(error=e))

    locale = get_object_or_404(Locale, code=l)

    entities = (Entity.objects.filter(
        pk__in=entity_pks).prefetch_resources_translations(locale))

    if not entities.exists():
        return JsonResponse({'count': 0})

    # Batch editing is only available to translators.
    # Check if user has translate permissions for all of the projects in passed entities.
    projects = Project.objects.filter(pk__in=entities.values_list(
        'resource__project__pk', flat=True).distinct())
    for project in projects:
        if not request.user.can_translate(project=project, locale=locale):
            return HttpResponseForbidden(
                "Forbidden: You don't have permission for batch editing")

    translation_pks = set()

    for entity in entities:
        if entity.string_plural == "":
            translation_pks.add(entity.get_translation()['pk'])

        else:
            for plural_form in range(0, locale.nplurals or 1):
                translation_pks.add(entity.get_translation(plural_form)['pk'])

    translation_pks.discard(None)
    translations = Translation.objects.filter(pk__in=translation_pks)
    latest_translation_pk = None
    changed_translation_pks = []

    # Must be executed before translations set changes, which is why
    # we need to force evaluate QuerySets by wrapping them inside list()
    def get_translations_info(translations):
        count = translations.count()
        translated_resources = list(translations.translated_resources(locale))
        changed_entities = list(
            Entity.objects.filter(translation__in=translations).distinct())

        return count, translated_resources, changed_entities

    if action == 'approve':
        translations = translations.filter(approved=False)
        changed_translation_pks = list(
            translations.values_list('pk', flat=True))
        if changed_translation_pks:
            latest_translation_pk = translations.last().pk
        count, translated_resources, changed_entities = get_translations_info(
            translations)
        translations.update(approved=True,
                            approved_user=request.user,
                            approved_date=timezone.now())

    elif action == 'delete':
        count, translated_resources, changed_entities = get_translations_info(
            translations)
        translations.delete()

    elif action == 'replace':
        find = request.POST.get('find')
        replace = request.POST.get('replace')

        try:
            translations, changed_translations = translations.find_and_replace(
                find, replace, request.user)
            changed_translation_pks = [c.pk for c in changed_translations]
            if changed_translation_pks:
                latest_translation_pk = max(changed_translation_pks)
        except Translation.NotAllowed:
            return JsonResponse({
                'error': 'Empty translations not allowed',
            })

        count, translated_resources, changed_entities = get_translations_info(
            translations)

    if count == 0:
        return JsonResponse({'count': 0})

    # Update stats
    for translated_resource in translated_resources:
        translated_resource.calculate_stats(save=False)

    bulk_update(translated_resources,
                update_fields=[
                    'total_strings',
                    'approved_strings',
                    'fuzzy_strings',
                    'translated_strings',
                ])

    project = entity.resource.project
    project.aggregate_stats()
    locale.aggregate_stats()
    ProjectLocale.objects.get(locale=locale, project=project).aggregate_stats()

    # Mark translations as changed
    changed_entities_array = []
    existing = ChangedEntityLocale.objects.values_list('entity',
                                                       'locale').distinct()
    for changed_entity in changed_entities:
        key = (changed_entity.pk, locale.pk)

        # Remove duplicate changes to prevent unique constraint violation
        if not key in existing:
            changed_entities_array.append(
                ChangedEntityLocale(entity=changed_entity, locale=locale))
    ChangedEntityLocale.objects.bulk_create(changed_entities_array)

    # Update latest translation
    if latest_translation_pk:
        Translation.objects.get(
            pk=latest_translation_pk).update_latest_translation()

    # Update translation memory
    memory_entries = [
        TranslationMemoryEntry(
            source=t.entity.string,
            target=t.string,
            locale=locale,
            entity=t.entity,
            translation=t,
            project=project,
        ) for t in Translation.objects.filter(pk__in=changed_translation_pks).
        prefetch_related('entity__resource')
    ]
    TranslationMemoryEntry.objects.bulk_create(memory_entries)

    return JsonResponse({'count': count})
Example #6
0
def ajax_notifications(request, slug):
    """Notifications tab."""
    project = get_object_or_404(Project.objects.available(), slug=slug)
    available_locales = project.locales.order_by('name')

    # Send notifications
    if request.method == 'POST':
        form = forms.NotificationsForm(request.POST)

        if not form.is_valid():
            return JsonResponse(dict(form.errors.items()))

        contributors = User.objects.filter(
            translation__entity__resource__project=project,
        )

        # For performance reasons, only filter contributors for selected
        # locales if different from all project locales
        available_ids = sorted(list(available_locales.values_list('id', flat=True)))
        selected_ids = sorted(split_ints(form.cleaned_data.get('selected_locales')))

        if available_ids != selected_ids:
            contributors = User.objects.filter(
                translation__entity__resource__project=project,
                translation__locale__in=available_locales.filter(id__in=selected_ids)
            )

        identifier = uuid.uuid4().hex
        for contributor in contributors.distinct():
            notify.send(
                request.user,
                recipient=contributor,
                verb='has sent a message in',
                target=project,
                description=form.cleaned_data.get('message'),
                identifier=identifier
            )

    # Detect previously sent notifications using a unique identifier
    # TODO: We should simplify this with a custom Notifications model
    notifications = []

    identifiers = set(list(Notification.objects.filter(
        description__isnull=False,
        target_content_type=ContentType.objects.get_for_model(project),
        target_object_id=project.id
    ).values_list("data", flat=True)))

    for identifier in identifiers:
        notifications.append(Notification.objects.filter(data__contains=identifier)[0])

    notifications.sort(key=lambda x: x.timestamp, reverse=True)

    # Recipient shortcuts
    incomplete = []
    complete = []
    for available_locale in available_locales:
        completion_percent = available_locale.get_chart(project)['completion_percent']
        if completion_percent == 100:
            complete.append(available_locale.pk)
        else:
            incomplete.append(available_locale.pk)

    return render(request, 'projects/includes/manual_notifications.html', {
        'form': forms.NotificationsForm(),
        'project': project,
        'available_locales': available_locales,
        'notifications': notifications,
        'incomplete': incomplete,
        'complete': complete,
    })
Example #7
0
 def clean_entities(self):
     return utils.split_ints(self.cleaned_data['entities'])
Example #8
0
def batch_edit_translations(request):
    try:
        l = request.POST['locale']
        action = request.POST['action']
        entity_pks = split_ints(request.POST.get('entities', ''))
    except MultiValueDictKeyError as e:
        return HttpResponseBadRequest('Bad Request: {error}'.format(error=e))

    locale = get_object_or_404(Locale, code=l)

    entities = (
        Entity.objects.filter(pk__in=entity_pks)
        .prefetch_resources_translations(locale)
    )

    if not entities.exists():
        return JsonResponse({'count': 0})

    # Batch editing is only available to translators.
    # Check if user has translate permissions for all of the projects in passed entities.
    projects = Project.objects.filter(pk__in=entities.values_list('resource__project__pk', flat=True).distinct())
    for project in projects:
        if not request.user.can_translate(project=project, locale=locale):
            return HttpResponseForbidden(
                "Forbidden: You don't have permission for batch editing"
            )

    translation_pks = set()

    for entity in entities:
        if entity.string_plural == "":
            translation_pks.add(entity.get_translation()['pk'])

        else:
            for plural_form in range(0, locale.nplurals or 1):
                translation_pks.add(entity.get_translation(plural_form)['pk'])

    translation_pks.discard(None)
    translations = Translation.objects.filter(pk__in=translation_pks)
    latest_translation_pk = None
    changed_translation_pks = []

    # Must be executed before translations set changes, which is why
    # we need to force evaluate QuerySets by wrapping them inside list()
    def get_translations_info(translations):
        count = translations.count()
        translated_resources = list(translations.translated_resources(locale))
        changed_entities = list(Entity.objects.filter(translation__in=translations).distinct())

        return count, translated_resources, changed_entities

    if action == 'approve':
        translations = translations.filter(approved=False)
        changed_translation_pks = list(translations.values_list('pk', flat=True))
        if changed_translation_pks:
            latest_translation_pk = translations.last().pk
        count, translated_resources, changed_entities = get_translations_info(translations)
        translations.update(
            approved=True,
            approved_user=request.user,
            approved_date=timezone.now()
        )

    elif action == 'delete':
        count, translated_resources, changed_entities = get_translations_info(translations)
        TranslationMemoryEntry.objects.filter(translation__in=translations).delete()
        translations.delete()

    elif action == 'replace':
        find = request.POST.get('find')
        replace = request.POST.get('replace')

        try:
            translations, changed_translations = translations.find_and_replace(find, replace, request.user)
            changed_translation_pks = [c.pk for c in changed_translations]
            if changed_translation_pks:
                latest_translation_pk = max(changed_translation_pks)
        except Translation.NotAllowed:
            return JsonResponse({
                'error': 'Empty translations not allowed',
            })

        count, translated_resources, changed_entities = get_translations_info(translations)

    if count == 0:
        return JsonResponse({'count': 0})

    # Update stats
    for translated_resource in translated_resources:
        translated_resource.calculate_stats(save=False)

    bulk_update(translated_resources, update_fields=[
        'total_strings',
        'approved_strings',
        'fuzzy_strings',
        'translated_strings',
    ])

    project = entity.resource.project
    project.aggregate_stats()
    locale.aggregate_stats()
    ProjectLocale.objects.get(locale=locale, project=project).aggregate_stats()

    # Mark translations as changed
    changed_entities_array = []
    existing = ChangedEntityLocale.objects.values_list('entity', 'locale').distinct()
    for changed_entity in changed_entities:
        key = (changed_entity.pk, locale.pk)

        # Remove duplicate changes to prevent unique constraint violation
        if key not in existing:
            changed_entities_array.append(
                ChangedEntityLocale(entity=changed_entity, locale=locale)
            )
    ChangedEntityLocale.objects.bulk_create(changed_entities_array)

    # Update latest translation
    if latest_translation_pk:
        Translation.objects.get(pk=latest_translation_pk).update_latest_translation()

    # Update translation memory
    memory_entries = [TranslationMemoryEntry(
        source=t.entity.string,
        target=t.string,
        locale=locale,
        entity=t.entity,
        translation=t,
        project=project,
    ) for t in Translation.objects.filter(pk__in=changed_translation_pks).prefetch_related('entity__resource')]
    TranslationMemoryEntry.objects.bulk_create(memory_entries)

    return JsonResponse({
        'count': count
    })
Example #9
0
def entities(request):
    """Get entities for the specified project, locale and paths."""
    try:
        project = request.POST['project']
        locale = request.POST['locale']
        paths = request.POST.getlist('paths[]')
        limit = int(request.POST.get('limit', 50))
    except (MultiValueDictKeyError, ValueError) as err:
        return HttpResponseBadRequest('Bad Request: {error}'.format(error=err))

    project = get_object_or_404(Project, slug=project)
    locale = get_object_or_404(Locale, code=locale)

    status = request.POST.get('status', '')
    extra = request.POST.get('extra', '')
    time = request.POST.get('time', '')
    author = request.POST.get('author', '')
    search = request.POST.get('search', '')
    exclude_entities = split_ints(request.POST.get('excludeEntities', ''))

    # Only return entities with provided IDs (batch editing)
    entity_ids = split_ints(request.POST.get('entityIds', ''))

    if entity_ids:
        entities = (
            Entity.objects.filter(pk__in=entity_ids)
            .prefetch_resources_translations(locale)
            .distinct()
            .order_by('order')
        )

        return JsonResponse({
            'entities': Entity.map_entities(locale, entities),
            'stats': TranslatedResource.objects.stats(project, paths, locale),
        }, safe=False)

    entities = Entity.for_project_locale(
        project, locale, paths, status, search, exclude_entities, extra, time, author
    )

    # Only return a list of entity PKs (batch editing: select all)
    if request.POST.get('pkOnly', None):
        return JsonResponse({
            'entity_pks': list(entities.values_list('pk', flat=True)),
        })

    visible_entities = []

    # In-place view: load all entities
    if request.POST.get('inplaceEditor', None):
        has_next = False
        entities_to_map = Entity.for_project_locale(
            project, locale, paths, None, None, exclude_entities
        )
        visible_entities = entities.values_list('pk', flat=True)

    # Out-of-context view: paginate entities
    else:
        paginator = Paginator(entities, limit)

        try:
            entities_page = paginator.page(1)
        except EmptyPage:
            return JsonResponse({
                'has_next': False,
                'stats': {},
                'authors': [],
                'counts_per_minute': [],
            })

        has_next = entities_page.has_next()
        entities_to_map = entities_page.object_list

        # If requested entity not on the first page
        entity = request.POST.get('entity', None)
        if entity:
            try:
                entity_pk = int(entity)
            except ValueError as err:
                return HttpResponseBadRequest('Bad Request: {error}'.format(error=err))

            # TODO: entities_to_map.values_list() doesn't return entities from selected page
            if entity_pk not in [e.pk for e in entities_to_map]:
                if entity_pk in entities.values_list('pk', flat=True):
                    entities_to_map = list(entities_to_map) + list(entities.filter(pk=entity_pk))

    return JsonResponse({
        'entities': Entity.map_entities(locale, entities_to_map, visible_entities),
        'has_next': has_next,
        'stats': TranslatedResource.objects.stats(project, paths, locale),
    }, safe=False)
Example #10
0
 def clean_entities(self):
     return utils.split_ints(self.cleaned_data['entities'])