«Где моя кнопка? Почему она неактивна?» Как часто программист может слышать эти слова от пользователей своего продукта? Скорее всего, более чем очень часто для того, чтобы задуматься, а может быть продукт сам должен отвечать на этот вопрос пользователей.

Вот приблизительно так выглядит обычная неактивная кнопка, которая, по какой-либо причине, в данный момент не доступна пользователю:

Неактивная молчаливая кнопка
Она безмолвна, не интерактивна и не представляет пользователю ни малейшей информации о том, почему она находится в этом состоянии и при каких условиях станет активной. А вот как могла бы выглядеть «облагороженная», более информативная неактивная кнопка:

Неактивная облагороженная кнопка

В чем же соль, спросите вы? А вот о том, как можно описывать правила на языке программирования Python, чтобы эти правила одновременно вычисляли статус активности кнопки и предоставляли отчет о причине в случае если кнопка должна быть деактивирована, я бы и хотел рассказать в этой статье.

Используя библиотеку собственного изготовления можно описать следующее правило на языке программирования Python:

from rules import _and_, _or_

# The rule
def can_user_add_comment(user, topic):
    return _and_(
        lambda: (user, “Вы не авторизованы”),
        lambda: (user.is_status_active(), “Ваша учетная запись заблокирована”),
        lambda: _or_(
            lambda: (user.is_in_admin_group(), “Вы не администратор”),
            lambda: _and_(
                lambda: (topic.is_status_open(), “тема закрыта”),
                lambda: (user.has_enough_karma_to_add_comment(), “у Вас не достаточно кармы для комментирования”),
                lambda: (user.is_below_comments_per_minute_quota(), “Вы комментируете слишком интенсивно, повторите попытку через пару минут”),
            ),
        ),
    )

Классы логических операций принимают операнды, обернутые в lambda, которые будут вызваны для вычисления значений при необходимости. Т.е. вычисления происходят по «укороченной схеме», например, _and_ вычисляет значения своих аргументов до первого False, _or_ — до первого True. Каждый операнд состоит из собственно тестируемого значения (первый элемент кортежа) и сообщения (второй элемент кортежа), которое используется когда тестируемое значение приняло значение False. Так же, при необходимости, можно опускать сообщения для не значимых частей правила (операндов) с точки зрения информирования пользователя.

Тогда использовать это правило, например в шаблоне Mako, можно следующим образом:

<%
    user = get_authenticated_user()
    granted = can_user_add_comment(user, current_topic)
%>
<input 
    type=”button” 
    value=”Добавить комментарий” 
    title=”${”Добавить комментарий” if granted else unicode(granted)}”
    ${“disabled” if not granted else “”}
/>

Как видно из примера, объект granted, полученный в результате вызова функции can_user_add_comment(), в булевом контексте сообщает статус активности кнопки, а в строковом контексте сообщает причину, если в булевом контексте он вернул значение False. Причина неактивности кнопки записывается в ее title и извлекается пользователем при наведении на нее курсора мыши. По умолчанию в отчет попадают только конкретные сообщения, результат вычисления правил которых оказался False. Но так же возможно сгенерировать полный древовидный отчет о том, какие логические ветки всего правила вернули значение False. Для этого надо вызвать granted.build_report() или granted.build_html_report().

В арсенале библиотеки доступны операции _and_, _or_, _not_ и _true_.

В результате поиска в сети мне не удалось обнаружить что-либо похожее, поэтому пришлось написать свою библиотеку. Первая версия была выпущена совсем недавно. Мне будет очень интересно узнать о вашем опыте решения этой маленькой, но иногда довольно актуальной, задачи.