Свой приватный Интернет-клуб (на платформе Vas3k.Club) #3
Вот мы и добрались до третьей части статей о создании собственного Клуба на платформе Vas3k'а. С предыдущими сериями можно ознакомиться тут:
- Делаем форк и поднимаем базовые сервисыЧасть 1: https://teletype.in/@toptuk/pmiclub1
- Готовим конфигурационные файл и поднимаем клуб локально: https://teletype.in/@toptuk/pmiclub2
В этой части мы сделаем следующее:
- Включим регистрацию в Клубе без оплаты.
- Настроим список тегов для профилей пользователей.
- Создадим Администратора (с правами GOD).
- Кастомизируем некоторые страницы Клуба.
Бесплатная регистрация (вход без оплаты)
По умолчанию, членство в клубе Vas3k'а - платное. Вы можете либо реализовать свою систему регистрации в Клубе (тут я не смогу помочь в этой статье), либо захотите хотя бы на время включить бесплатную регистрацию.
В зависимости от назначения вашего клуба, бесплатная регистрация может и не понадобиться. Если бесплатная регистрация вам не нужна, то придется покодировать что-то свое!
Vas3k рекомендует делать новые функции Клуба через механизм FeatureFlag'ов. Ну тоже тут поделать, кроме как незамедлительно приступить!?
Добавление Feature Flag
Открываем файл club/features.py и добавляем в конец новый фича флаг:
# Enable Free membership # True - Free registration # False - Paid registration FREE_MEMBERSHIP = True
Подсмотреть реализацию можно тут: https://github.com/TopTuK/pmi.moscow.club/blob/master/club/features.py
Включаем вход без оплаты
Открываем файл auth/views/emai.py и находим там функцию def email_login(request).
Переписываем эту функцию следующим образом:
def email_login(request): if request.method != "POST": return redirect("login") goto = request.POST.get("goto") email_or_login = request.POST.get("email_or_login") if not email_or_login: return redirect("login") email_or_login = email_or_login.strip() if "|-" in email_or_login: # secret_hash login email_part, secret_hash_part = email_or_login.split("|-", 1) user = User.objects.filter(email=email_part, secret_hash=secret_hash_part).first() if not user: return render(request, "error.html", { "title": "Такого юзера нет 🤔", "message": "Пользователь с таким кодом не найден. " "Попробуйте авторизоваться по обычной почте или юзернейму.", }, status=404) if user.deleted_at: # cancel user deletion user.deleted_at = None user.save() session = Session.create_for_user(user) redirect_to = reverse("profile", args=[user.slug]) if not goto else goto response = redirect(redirect_to) return set_session_cookie(response, user, session) else: # email/nickname login # Вот, что добавлено нового. Тут происходит регистрация нового пользователя на период в один год if features.FREE_MEMBERSHIP: now = datetime.utcnow() try: user, _ = User.objects.get_or_create( email=email_or_login.lower(), defaults=dict( membership_platform_type=User.MEMBERSHIP_PLATFORM_DIRECT, full_name=email_or_login[:email_or_login.find("@")], membership_started_at=now, membership_expires_at=now + timedelta(days=365), created_at=now, updated_at=now, moderation_status=User.MODERATION_STATUS_INTRO, ), ) except IntegrityError: return render(request, "error.html", { "title": "Что-то пошло не так 🤔", "message": "Напишите нам, и мы всё починим. Или попробуйте ещё раз.", }, status=404) else: user = User.objects.filter(Q(email=email_or_login.lower()) | Q(slug=email_or_login)).first() if not user: return render(request, "error.html", { "title": "Такого юзера нет 🤔", "message": "Пользователь с такой почтой не найден в списке членов Клуба. " "Попробуйте другую почту или никнейм. " "Если совсем ничего не выйдет, напишите нам, попробуем помочь.", }, status=404) code = Code.create_for_user(user=user, recipient=user.email, length=settings.AUTH_CODE_LENGTH) async_task(send_auth_email, user, code) async_task(notify_user_auth, user, code) return render(request, "auth/email.html", { "email": user.email, "goto": goto, "restore": user.deleted_at is not None, })
Функция актуальна на момент написания этой статьи :) Если в будущем что-то изменится, то для нас важно сохранить логику - регистрируем нового пользователя на 1 год под фича-флагом.
Если пользователь регистрируется с новым Email проверяется Feature Flag FREE_MEMBERSHIP. Если включена бесплатная регистрация, то создается новый пользователь, ему отправляется письмо с кодом для авторизации для написания Intro. По умолчанию, создаются пользователи с годовой (365 дней) подпиской.
Исправляем главную страницу (Landing)
В данном файле нужно что-то сделать с блоком про платную подписку:
<div class="landing-block landing-block-narrow" id="join"> <div> <h2 class="landing-block-title">Как вступить?</h2> <img src="{% static "images/landing/dolor.png" %}" alt="$1" class="landing-dolor"> <p> Членство в Клубе платное. Это позволяет нам оставаться независимыми от мнения инвесторов и рекламодателей. </p> <p> Плюс, мы не пускаем анонимов. Вам придется заполнить профиль и рассказать о себе. </p> <br> <p> <a href="{% url "join" %}" class="button button-black button-big">Вступить в Клуб</a> </p> </div> </div>
Будем использовать наш FeatureFlag FREE_MEMBERSHIP. Я предлагаю сделать так:
{% if not features.FREE_MEMBERSHIP %} ... ТУТ БЛОК ПРО ПЛАТНУЮ ПОДПИСКУ ... {% else %} <div class="landing-block landing-block-narrow" id="join"> ... ТУТ БЛОК ПРО БЕСПЛАТНЫЙ ВХОД ... </div> {% endif %}
Подсмотреть, как сделано у меня можно тут: landing.html
Правим страницу для входа (Login)
Нужно скрыть "лишнюю" кнопку входа. Получиться должно как-то так:
{% if not features.FREE_MEMBERSHIP %} <div class="login-separator">🤔 Всё еще нет клубной карты?</div> <div class="login-join"> <a href="{% url "join" %}" class="button">Вступить в Клуб</a> </div> {% endif %}
Подсмотреть можно тут: login.html
Актуализируем страницу Контактов (Contact)
По аналогии с login.html, скрываем упоминание про платную подписку:
{% if not features.FREE_MEMBERSHIP %} <h3>Проблемы с оплатой или возврат денег</h3> <p> Ошибки при списании оплаты — самое отвратительное, что только может случиться в интернете. Главное — не волнуйтесь. Мы всегда здесь и можем найти и даже откатить любую транзакцию. </p> <p> Просто напишите на <strong><a href="mailto:money@pmi.moscow">money@pmi.moscow</a></strong> и мы решим всё в приоритете. </p> {% endif %}
Подсмотреть код можно здесь: contact.html
Правим страницу редактирования профиля
На данной страничке нам надо скрыть иконку "Деньги": Обрамим соответствующий <div> в условную конструкцию:
{% if not features.FREE_MEMBERSHIP %} <a href="{% url "edit_payments" user.slug %}" class="dashboard-item"> <span class="dashboard-item-icon">💰</span> <span class="dashboard-item-title">Деньги</span> </a> {% endif %}
Пример файла тут: edit_index.html
Актуализируем страницу с оплатой
Сложный пример, но нам нужно вынести все про оплату Клуба в отдельный блок. Посмотрите исходный файл: payments.html
{% else %} <div class="block-description"> Членство в клубе пока бесплатное. Для продления членства необходимо обратиться к великолепным модераторам и волонтерам. </div> <div class="block-header" style="max-width: 600px; margin-top: 100px;">🏅<br>А чтобы было веселее, вот топ членов Клуба с самой длинной подпиской</div> {% endif %}
Редактируем теги для профилей
Для создания своего Клуба нужно придумать собственный список тегов для профиля или же использовать профили от Vas3k'а.
Список тегов редактируется в файле common/data/tags.py. Обращаю внимание, что используются кортежи (Tuple, т.е. круглые скобки), а не список или словарь.
Автор словил приход пока отлаживал код тегов из-за того, что вместо круглых скобок использовал фигурные. Ни тесты, ни сборка не возвращали проблем, но ничего не работало.
Наверняка вы обратили внимание во второй части статей на команду:
# Запускаем обновление тегов для профилей пользователей (Без этой команды теги по умолчанию будут пустыми) pipenv run python manage.py update_tags
Вы все правильно поняли. Если меняете теги для уже запущенного Клуба, то для обновления вам потребуется выполнять данную команду каждый раз.
Обратите внимание на то, какие результаты возвращает данная команда:
Подсмотреть, как это сделано у нас в Клубе можно тут: tags.py
Для того, чтобы обновить теги в Клубе, который запущен с помощью команды docker-compose up -d нужно выполнить следующую последовательность команд:
* docker exec -it club_app (или идентификатор сервиса Клуба) /bin/sh
* python3 manage.py update_tags
Если что-то пошло не так, то надо в контейнере перейти в директорию, где размещены исходные файлы клуба.
Создаем Администратора (с правами GOD)
Клуб, запущенный в режиме отладки (DEBUG), позволяет создавать тестовых пользователей. Для создания пользователей нужно открыть в браузере следующие страницы:
- Для создания Администратора: http://localhost:8000/godmode/dev_login/
- Для создания тестового пользователя: http://localhost:8000/godmode/random_login/
В режиме PRODUCTION создание тестовых пользователей будет невозможно. Ниже описан алгоритм, что нужно сделать для создания Администратора Клуба (т.е. себя любимого).
В зависимости от способа запуска Клуба, алгоритм создания Администратора немного различается. Ниже описаны 2 алгоритма создания пользователя с правами GOD.
Клуб запущен локально
В случае, если Клуб запущен локально (см. 2 часть)
Если включен бесплатная регистрация, то на этом шаге будет создан новый пользователь.
Если у вас другой алгоритм авторизации, то нужно создать пользователя вручную через Django Admin. Если вы умеете делать "свою" авторизацию, то и с ручным созданием пользователя справитесь. Пишите в комментарии, если нужна помощь.
- Зайти на сайт: http://localhost:8000
- Перейти на страницу для входа (http://localhost:8000/auth/login/).
- Ввести email адрес Администратора и инициировать вход.
- Зайти в почту Администратора, скопировать код авторизации.
- Ввести полученный код.
- Заполнить Intro о себе. Можно заполнить тестовыми данными.
- Отправить его на модерацию (т.к. в Клубе пока не зарегистрировано ни одного Модератора, то следующие шаги исправят это недаруземение)
- Зайти в Django Admin. Для этого выполнить в консоли команду:
pipenv run python3 manage.py shell
# Добавить модули: from users.models.user import User from datetime import datetime # Получить первого пользователя: user = User.objects.first() # Установить статус, что пользователь прошел модерацию user.moderation_status = User.MODERATION_STATUS_APPROVED user.created_at = datetime.utcnow() # Назначить роль GOD user.roles.append(User.ROLE_GOD) # Сохранить пользователя user.save() # Сделать интро Администратора видимым всем from posts.models.post import Post intro = Post.objects.filter(author=user, type=Post.TYPE_INTRO).first() intro.is_approved_by_moderator = True intro.is_visible = True intro.last_activity_at = datetime.utcnow() intro.save() # Актуализировать SearchIndex from search.models import SearchIndex SearchIndex.update_user_index(user)
Готово! Теперь созданный пользователь является Администратором Клуба. Он может осуществлять модерацию новых пользователей, аппрувить и постить посты.
Модерация Клуба осуществляется с помощью Телеграмм бота. Т.к. Клуб запущен локально, то Телеграм бот не работает.
Для выхода из консоли Django нужно вызвать функцию exit()
Клуб запущен с помощью docker-compose
В случае, если Клуб запущен с использованием команды docker-compose (т.е. Клуб запускается в Docker, см. 1 часть), то указанный выше способ создания первого пользователя и назначение ему роли Администратора не подходит.
Указанный способ также необходим для создания пользователя-Администратора для Клуба, запущенного в PRODUCTION среде.
- Создаем первого пользователя. В браузере переходим в Клуб:
- Зайти на сайт: http://localhost:8000
- Перейти на страницу для входа (http://localhost:8000/auth/login/).
- Ввести email адрес Администратора и инициировать вход.
- Зайти в почту Администратора, скопировать код авторизации.
- Ввести полученный код.
- Заполнить Intro о себе. (В случае, если Клуб запущен локально, то интро можно заполнить тестовыми данными)
- Отправить его на модерацию.
- Получаем список запущенных docker-сервисов. В консоли выполняем команду:
docker ps
docker exec -it <Идентификатор сервиса club_app> /bin/sh
python3 manage.py shell
- Выполнить последовательность команд в Django Admin (построчно, кроме комментариев. Команды аналогичные тем, что использовались для Клуба, запущенного локально)
# Добавить модули: from users.models.user import User from datetime import datetime # Получить первого пользователя: user = User.objects.first() # Установить статус, что пользователь прошел модерацию user.moderation_status = User.MODERATION_STATUS_APPROVED user.created_at = datetime.utcnow() # Назначить роль GOD user.roles.append(User.ROLE_GOD) # Сохранить пользователя user.save() # Сделать интро Администратора видимым всем from posts.models.post import Post intro = Post.objects.filter(author=user, type=Post.TYPE_INTRO).first() intro.is_approved_by_moderator = True intro.is_visible = True intro.last_activity_at = datetime.utcnow() intro.save() # Актуализировать SearchIndex from search.models import SearchIndex SearchIndex.update_user_index(user)
Для выхода нужно выполнить следующую последовательность команд:
# Выход из консоли Django exit() # Выход из консоли сервиса club_app exit
Заключение
- Как реализовать бесплатную регистрацию в Клубе.
- Как актуализировать теги для профилей Пользователя и применить обновления.
- Как создать первого пользователя в Клубе и назначить ему роль Администратора с правами GOD.
На данный момент у нас уже должны работать сервисы:
В следующей части мы реализуем:
- Выкладку и запуск Клуба в PRODUCTION среде.
- Настройку автоматизированного Deploy'я Клуба с помощью Github Actions.