Перейти к содержанию

Реализация скрытых уровней иерархии

Это техническая документация для разработчиков. Документацию для пользователей можно найти в разделе Работа со скрытыми уровнями иерархии.

Обзор

Функциональность скрытых уровней иерархии реализована на уровне модели Level с использованием стандартных механизмов Django для прав доступа и кастомной логики фильтрации в API и интерфейсе.

Модель Level

В модели Level определено поле is_hidden:

is_hidden = models.BooleanField(
    default=False,
    verbose_name=_('is_hidden'),
    help_text=_('Технические поля, скрытые от пользователя. Требуется специальное разрешение для просмотра.')
)

В Meta-классе модели определено специальное разрешение:

class Meta:
    permissions = [
        ("view_hidden_levels", _("Может просматривать скрытые уровни иерархии")),
    ]

API и сериализация

Сериализаторы

В системе используются следующие сериализаторы для работы с уровнями:

  1. LevelGenericSerializer - базовый сериализатор для всех уровней:

    class Meta:
        model = HierarchyLevel
        fields = ("id", "type", "child", "key", "name", "description", "parents", "is_hidden")
        extra_kwargs = {
            "is_hidden": {"read_only": True},
        }
    

  2. HierarchyLevelChoiceSerializer - для выбора уровней:

    class Meta:
        model = HierarchyLevel
        fields = ("id", "label", "name", "key", "is_hidden")
    

  3. HierarchyLevelSerializer - полный сериализатор для уровней:

    class Meta:
        model = HierarchyLevel
        fields = (
            "id", "type", "child", "key", "name", "description", "parents",
            "use_conversion_factors", "mappings", "is_hidden",
            "show_shortname", "show_description", "is_calendar"
        )
        extra_kwargs = {
            "is_hidden": {"read_only": True},
        }
    

Фильтрация в API

Фильтрация скрытых уровней реализована на уровне API-представлений и функций запроса данных.

Пользователи БЕЗ разрешения view_hidden_levels: - Не видят скрытые уровни (is_hidden=True) в списке уровней - Не видят скрытые уровни в выпадающих списках и фильтрах - Поле is_hidden присутствует в ответах API для всех возвращенных уровней

Пользователи С разрешением view_hidden_levels: - Видят все уровни, включая скрытые (is_hidden=True) - Поле is_hidden присутствует в ответах API для всех уровней

Места реализации фильтрации: 1. HierarchyLevelViewSet.get_queryset() - API для уровней иерархии 2. get_levels_with_descendants() - получение уровней с потомками для отчетов и фильтров 3. get_level_filters() - получение фильтров по уровням

Интеграция с интерфейсом

Административный интерфейс

В административном интерфейсе Django (/admin/) для модели Level: 1. Поле is_hidden отображается в списке уровней 2. Доступно для редактирования при создании/изменении уровня 3. Включено в основные поля формы

Фронтенд

Передача прав доступа

Права доступа к скрытым уровням передаются на клиент через:

  1. В дашбордах (dashboard-view.html):

    <input type="hidden" id="user-has-hidden-levels-permission" value="{{ userHasHiddenLevelsPermission }}">
    

  2. В отчетах (report-view.html):

    context = {
        'report': report,
        'userHasHiddenLevelsPermission': request.user.has_perm('hierarchy.view_hidden_levels')
    }
    

Реализация в отчетах

В отчетах скрытие уровней реализовано в spread_manager.js:

  1. При инициализации меню отчетов (_initReportMenu):

    // Получаем значение разрешения на просмотр скрытых уровней один раз
    var userHasHiddenLevelsPermission = self._userHasHiddenLevelsPermission();
    
    _.forEach(self.options.pages, function (page) {
        var pageSelect;
        var levelInfo = _.find(self._levelsInfo, {id: page.level});
        var isHidden = levelInfo && levelInfo.is_hidden;
    
        combobox = $('<select class="form-control"></select>')
            .attr('data-id', page.level)
            .attr('data-name', page.name)
            .attr('data-type', 'page-dimension');
        comboboxes.push(combobox);
    
        pageSelect = $('<div class="us-report-page-select"></div>');
        if (isHidden && !userHasHiddenLevelsPermission) {
            pageSelect.addClass('hidden');
        }
        pageSelect.append('<div class="pull-left"><label>' + page.description + ': </label><div> ');
        pageSelect.append($('<div class="pull-left"></div>').append(combobox));
        menu.append(pageSelect);
    });
    

  2. При получении данных об уровнях:

    // Получаем информацию об уровнях иерархии
    var levelIds = _.map(self.options.pages, 'level');
    if (levelIds.length > 0) {
        $.us.hierarchy.ajax.levels(levelIds, function(err, data) {
            cb(err, data || []);
        });
    }
    

Фильтрация в выпадающих списках

  1. В компонентах выбора уровней (us.hierarchy-item-classifiers):

    function forEachParentHierarchyLevel(action) {
        if(!scope.parentHierarchyLevels) {
            HierarchyLevel.get({"id": scope.item.level}, function (hierarchyLevel) {
                scope.parentHierarchyLevels = _.filter(hierarchyLevel.parents_objs, function (obj) {
                    return !obj.is_hidden;
                });
                _.forEach(scope.parentHierarchyLevels, action);
            });
        }
    }
    

  2. В фильтрах иерархии (HierarchyFilter):

    def __init__(self, label="", levels=None, use_permissions=True):
        super().__init__(attrs=None)
        self.levels = levels
        self.use_permissions = use_permissions
    

Тестирование

Тесты для функциональности находятся в tests/core/hierarchy/test_permissions_hidden_levels.py:

  1. Проверка видимости скрытых уровней для обычных пользователей
  2. Проверка видимости скрытых уровней для администраторов
  3. Проверка наличия поля is_hidden в ответах API

Миграции

Миграция для добавления поля is_hidden и прав доступа:

class Migration(migrations.Migration):
    dependencies = [
        ("hierarchy", "0010_alter_level_mappings_alter_level_show_description"),
    ]

    operations = [
        migrations.AlterModelOptions(
            name="level",
            options={
                "permissions": [
                    ("view_hidden_levels", "Может просматривать скрытые уровни иерархии")
                ],
            },
        ),
        migrations.AlterField(
            model_name="level",
            name="is_hidden",
            field=models.BooleanField(
                default=False,
                help_text="Технические поля, скрытые от пользователя. Требуется специальное разрешение для просмотра.",
                verbose_name="is_hidden",
            ),
        ),
    ]

Использование в коде

Проверка прав доступа

Для проверки прав доступа к скрытым уровням используется стандартный механизм Django:

user.has_perm('hierarchy.view_hidden_levels')

Фильтрация уровней

При получении списка уровней необходимо учитывать права пользователя.

Универсальный паттерн (безопасный для всех случаев):

# Проверяем user на None, чтобы избежать AttributeError
if not user or not user.has_perm('hierarchy.view_hidden_levels'):
    levels = levels.filter(is_hidden=False)

Почему проверка if not user необходима:

  1. В ViewSet'ах: request.user обычно не None (благодаря AuthenticationMiddleware), но может быть в тестах или нестандартных конфигурациях

  2. В функциях: параметр user может быть явно передан как None

  3. Для безопасности: проверка защищает от AttributeError в edge cases

Важно: - В Django с AuthenticationMiddleware (строка 173 в settings.py) request.user всегда либо User, либо AnonymousUser, но не None - Однако для надежности (тесты, нестандартные конфигурации) рекомендуется всегда проверять if not user - Для AnonymousUser метод has_perm() всегда возвращает False - Для None вызов has_perm() вызовет AttributeError

Работа с API

При работе с API необходимо учитывать, что: 1. Поле is_hidden доступно только для чтения (read_only) 2. Поле is_hidden всегда присутствует в ответах API для всех пользователей 3. Скрытые уровни (is_hidden=True) не возвращаются в API для пользователей без разрешения view_hidden_levels 4. При создании/обновлении уровня поле is_hidden игнорируется в запросах (только для чтения)