Development
September 19, 2022

Свой приватный Интернет-клуб (на платформе Vas3k.Club) #3

Вот мы и добрались до третьей части статей о создании собственного Клуба на платформе Vas3k'а. С предыдущими сериями можно ознакомиться тут:

В этой части мы сделаем следующее:

  • Включим регистрацию в Клубе без оплаты.
  • Настроим список тегов для профилей пользователей.
  • Создадим Администратора (с правами 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)

Редактируем файл frontend/html/landing.html.

В данном файле нужно что-то сделать с блоком про платную подписку:

<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)

Редактируем файл frontend/html/auth/login.html

Нужно скрыть "лишнюю" кнопку входа. Получиться должно как-то так:

{% 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)

Редактируем файл frontend/html/docs/contact.html

По аналогии с login.html, скрываем упоминание про платную подписку:

{% if not features.FREE_MEMBERSHIP %}
  <h3>Проблемы с оплатой или возврат денег</h3>
  <p>
    Ошибки при списании оплаты — самое отвратительное, что только может случиться в интернете.
    Главное — не волнуйтесь. Мы всегда здесь и можем найти и даже откатить любую транзакцию.
  </p>
  
  <p>
    Просто напишите на <strong><a href="mailto:[email protected]">[email protected]</a></strong> и мы решим всё в приоритете.
  </p>
{% endif %}

Подсмотреть код можно здесь: contact.html

Правим страницу редактирования профиля

Редактируем файл frontend/html/users/edit/index.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

Актуализируем страницу с оплатой

Редактируем файл frontend/html/users/edit/payments.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), позволяет создавать тестовых пользователей. Для создания пользователей нужно открыть в браузере следующие страницы:

В режиме PRODUCTION создание тестовых пользователей будет невозможно. Ниже описан алгоритм, что нужно сделать для создания Администратора Клуба (т.е. себя любимого).

В зависимости от способа запуска Клуба, алгоритм создания Администратора немного различается. Ниже описаны 2 алгоритма создания пользователя с правами GOD.

Клуб запущен локально

В случае, если Клуб запущен локально (см. 2 часть)

  • Создать первого пользователя в базе данных:
Если включен бесплатная регистрация, то на этом шаге будет создан новый пользователь.
Если у вас другой алгоритм авторизации, то нужно создать пользователя вручную через Django Admin. Если вы умеете делать "свою" авторизацию, то и с ручным созданием пользователя справитесь. Пишите в комментарии, если нужна помощь.
    1. Зайти на сайт: http://localhost:8000
    2. Перейти на страницу для входа (http://localhost:8000/auth/login/).
    3. Ввести email адрес Администратора и инициировать вход.
    4. Зайти в почту Администратора, скопировать код авторизации.
    5. Ввести полученный код.
    6. Заполнить Intro о себе. Можно заполнить тестовыми данными.
    7. Отправить его на модерацию (т.к. в Клубе пока не зарегистрировано ни одного Модератора, то следующие шаги исправят это недаруземение)
  • Зайти в Django Admin. Для этого выполнить в консоли команду:
pipenv run 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()

Клуб запущен с помощью docker-compose

В случае, если Клуб запущен с использованием команды docker-compose (т.е. Клуб запускается в Docker, см. 1 часть), то указанный выше способ создания первого пользователя и назначение ему роли Администратора не подходит.

Указанный способ также необходим для создания пользователя-Администратора для Клуба, запущенного в PRODUCTION среде.

  • Создаем первого пользователя. В браузере переходим в Клуб:
    1. Зайти на сайт: http://localhost:8000
    2. Перейти на страницу для входа (http://localhost:8000/auth/login/).
    3. Ввести email адрес Администратора и инициировать вход.
    4. Зайти в почту Администратора, скопировать код авторизации.
    5. Ввести полученный код.
    6. Заполнить Intro о себе. (В случае, если Клуб запущен локально, то интро можно заполнить тестовыми данными)
    7. Отправить его на модерацию.
  • Получаем список запущенных docker-сервисов. В консоли выполняем команду:
docker ps
  • Ищем идентификатор сервиса club_app:
0d8d87d382bf - идентификатор сервиса club_app
  • Выполняем команду в консоли:
docker exec -it <Идентификатор сервиса club_app> /bin/sh
  • Заходим в консоль Django. Выполнить команду:
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.

На данный момент у нас уже должны работать сервисы:

  • PEPIC
  • OGIMGD
  • Телеграмм Бот

В следующей части мы реализуем:

  • Выкладку и запуск Клуба в PRODUCTION среде.
  • Настройку автоматизированного Deploy'я Клуба с помощью Github Actions.

Всем удачи в Клубостроительстве!

Meow!