Реализация скрытых уровней иерархии¶
Это техническая документация для разработчиков. Документацию для пользователей можно найти в разделе Работа со скрытыми уровнями иерархии.
Обзор¶
Функциональность скрытых уровней иерархии реализована на уровне модели 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 и сериализация¶
Сериализаторы¶
В системе используются следующие сериализаторы для работы с уровнями:
-
LevelGenericSerializer- базовый сериализатор для всех уровней:class Meta: model = HierarchyLevel fields = ("id", "type", "child", "key", "name", "description", "parents", "is_hidden") extra_kwargs = { "is_hidden": {"read_only": True}, } -
HierarchyLevelChoiceSerializer- для выбора уровней:class Meta: model = HierarchyLevel fields = ("id", "label", "name", "key", "is_hidden") -
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. Включено в основные поля формы
Фронтенд¶
Передача прав доступа¶
Права доступа к скрытым уровням передаются на клиент через:
-
В дашбордах (
dashboard-view.html):<input type="hidden" id="user-has-hidden-levels-permission" value="{{ userHasHiddenLevelsPermission }}"> -
В отчетах (
report-view.html):context = { 'report': report, 'userHasHiddenLevelsPermission': request.user.has_perm('hierarchy.view_hidden_levels') }
Реализация в отчетах¶
В отчетах скрытие уровней реализовано в spread_manager.js:
-
При инициализации меню отчетов (
_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); }); -
При получении данных об уровнях:
// Получаем информацию об уровнях иерархии var levelIds = _.map(self.options.pages, 'level'); if (levelIds.length > 0) { $.us.hierarchy.ajax.levels(levelIds, function(err, data) { cb(err, data || []); }); }
Фильтрация в выпадающих списках¶
-
В компонентах выбора уровней (
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); }); } } -
В фильтрах иерархии (
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:
- Проверка видимости скрытых уровней для обычных пользователей
- Проверка видимости скрытых уровней для администраторов
- Проверка наличия поля
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 необходима:
-
В ViewSet'ах:
request.userобычно неNone(благодаряAuthenticationMiddleware), но может быть в тестах или нестандартных конфигурациях -
В функциях: параметр
userможет быть явно передан какNone -
Для безопасности: проверка защищает от
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 игнорируется в запросах (только для чтения)