Exemple #1
0
def edit_profile(request, user_slug):
    if user_slug == "me":
        return redirect("edit_profile", request.me.slug, permanent=False)

    user = get_object_or_404(User, slug=user_slug)
    if user.id != request.me.id and not request.me.is_moderator:
        raise Http404()

    if request.method == "POST":
        form = UserEditForm(request.POST, request.FILES, instance=user)
        if form.is_valid():
            user = form.save(commit=False)
            user.save()

            SearchIndex.update_user_index(user)
            Geo.update_for_user(user)

            return redirect("profile", user.slug)
    else:
        form = UserEditForm(instance=user)

    return render(request, "users/edit/profile.html", {
        "form": form,
        "user": user
    })
Exemple #2
0
def unpublish_post(update: Update, context: CallbackContext) -> None:
    _, post_id = update.callback_query.data.split(":", 1)

    post = Post.objects.get(id=post_id)
    if not post.is_visible:
        update.effective_chat.send_message(
            f"Пост «{post.title}» уже перенесен в черновики")
        update.callback_query.edit_message_reply_markup(reply_markup=None)
        return None

    post.is_visible = False
    post.save()

    SearchIndex.update_post_index(post)

    notify_post_author_rejected(post)

    update.effective_chat.send_message(
        f"👎 Пост «{post.title}» перенесен в черновики ({update.effective_user.full_name})"
    )

    # hide buttons
    update.callback_query.edit_message_reply_markup(reply_markup=None)

    return None
Exemple #3
0
def approve_user_profile(update: Update, context: CallbackContext) -> None:
    _, user_id = update.callback_query.data.split(":", 1)

    user = User.objects.get(id=user_id)
    if user.moderation_status == User.MODERATION_STATUS_APPROVED:
        update.effective_chat.send_message(f"Пользователь «{user.full_name}» уже одобрен")
        return None

    if user.moderation_status == User.MODERATION_STATUS_REJECTED:
        update.effective_chat.send_message(f"Пользователь «{user.full_name}» уже был отклонен")
        return None

    user.moderation_status = User.MODERATION_STATUS_APPROVED
    user.save()

    # make intro visible
    Post.objects\
        .filter(author=user, type=Post.TYPE_INTRO)\
        .update(is_visible=True, published_at=datetime.utcnow(), is_approved_by_moderator=True)

    SearchIndex.update_user_index(user)

    notify_user_profile_approved(user)
    send_welcome_drink(user)

    update.effective_chat.send_message(
        f"✅ Пользователь «{user.full_name}» одобрен ({update.effective_user.full_name})"
    )

    # hide buttons
    update.callback_query.edit_message_reply_markup(reply_markup=None)

    return None
Exemple #4
0
def create_or_edit(request, post_type, post=None, mode="create"):
    FormClass = POST_TYPE_MAP.get(post_type) or PostTextForm

    # show blank form on GET
    if request.method != "POST":
        form = FormClass(instance=post)
        return render(request, f"posts/compose/{post_type}.html", {
            "mode": mode,
            "post_type": post_type,
            "form": form,
        })

    # validate form on POST
    form = FormClass(request.POST, request.FILES, instance=post)
    if form.is_valid():
        if not request.me.is_moderator:
            if Post.check_duplicate(user=request.me,
                                    title=form.cleaned_data["title"],
                                    ignore_post_id=post.id if post else None):
                raise ContentDuplicated()

            is_ok = Post.check_rate_limits(request.me)
            if not is_ok:
                raise RateLimitException(
                    title="🙅‍♂️ Слишком много постов",
                    message=
                    "В последнее время вы создали слишком много постов. Потерпите, пожалуйста."
                )

        post = form.save(commit=False)
        if not post.author_id:
            post.author = request.me
        post.type = post_type
        post.html = None  # flush cache
        post.save()

        if mode == "create" or not post.is_visible:
            PostSubscription.subscribe(request.me,
                                       post,
                                       type=PostSubscription.TYPE_ALL_COMMENTS)

        if post.is_visible:
            if post.topic:
                post.topic.update_last_activity()

            SearchIndex.update_post_index(post)
            LinkedPost.create_links_from_text(post, post.text)

        action = request.POST.get("action")
        if action == "publish":
            post.publish()
            LinkedPost.create_links_from_text(post, post.text)

        return redirect("show_post", post.type, post.slug)

    return render(request, f"posts/compose/{post_type}.html", {
        "mode": mode,
        "post_type": post_type,
        "form": form,
    })
Exemple #5
0
def edit_post(request, post_slug):
    post = get_object_or_404(Post, slug=post_slug)
    if post.author != request.me and not request.me.is_moderator:
        raise AccessDenied()

    PostFormClass = POST_TYPE_MAP.get(post.type) or PostTextForm

    if request.method == "POST":
        form = PostFormClass(request.POST, request.FILES, instance=post)
        if form.is_valid():
            post = form.save(commit=False)
            if not post.author:
                post.author = request.me
            post.html = None  # flush cache
            post.save()

            SearchIndex.update_post_index(post)
            LinkedPost.create_links_from_text(post, post.text)

            if post.is_visible:
                return redirect("show_post", post.type, post.slug)
            else:
                return redirect("compose")
    else:
        form = PostFormClass(instance=post)

    return render(request, f"posts/compose/{post.type}.html", {
        "mode": "edit",
        "form": form
    })
Exemple #6
0
def create_comment(request, post_slug):
    post = get_object_or_404(Post, slug=post_slug)
    if not post.is_commentable and not request.me.is_moderator:
        raise AccessDenied(title="Комментарии к этому посту закрыты")

    if request.POST.get("reply_to_id"):
        ProperCommentForm = ReplyForm
    elif post.type == Post.TYPE_BATTLE:
        ProperCommentForm = BattleCommentForm
    else:
        ProperCommentForm = CommentForm

    if request.method == "POST":
        form = ProperCommentForm(request.POST)
        if form.is_valid():
            is_ok = Comment.check_rate_limits(request.me)
            if not is_ok:
                raise RateLimitException(
                    title="🙅‍♂️ Вы комментируете слишком часто",
                    message=
                    "Подождите немного, вы достигли нашего лимита на комментарии в день. "
                    "Можете написать нам в саппорт, пожаловаться об этом.")

            comment = form.save(commit=False)
            comment.post = post
            if not comment.author:
                comment.author = request.me

            comment.ipaddress = parse_ip_address(request)
            comment.useragent = parse_useragent(request)
            comment.save()

            # update the shitload of counters :)
            request.me.update_last_activity()
            Comment.update_post_counters(post)
            PostView.increment_unread_comments(comment)
            PostView.register_view(
                request=request,
                user=request.me,
                post=post,
            )
            SearchIndex.update_comment_index(comment)
            LinkedPost.create_links_from_text(post, comment.text)

            return redirect("show_comment", post.slug, comment.id)
        else:
            log.error(f"Comment form error: {form.errors}")
            return render(
                request, "error.html", {
                    "title": "Какая-то ошибка при публикации комментария 🤷‍♂️",
                    "message": f"Мы уже получили оповещение и скоро пофиксим. "
                    f"Ваш коммент мы сохранили чтобы вы могли скопировать его и запостить еще раз:",
                    "data": form.cleaned_data.get("text")
                })

    raise Http404()
Exemple #7
0
def unpublish_post(post_id: str, update: Update) -> (str, bool):
    post = Post.objects.get(id=post_id)
    if not post.is_visible:
        return f"Пост «{post.title}» уже перенесен в черновики", True

    post.is_visible = False
    post.save()

    SearchIndex.update_post_index(post)

    notify_post_author_rejected(post)

    return f"👎 Пост «{post.title}» перенесен в черновики ({update.effective_user.full_name})", True
    def handle(self, *args, **options):
        SearchIndex.objects.all().delete()
        indexed_comment_count = 0
        indexed_post_count = 0
        indexed_user_count = 0

        for chunk in chunked_queryset(
            Comment.visible_objects().filter(is_deleted=False, post__is_visible=True)
        ):
            for comment in chunk:
                self.stdout.write(f"Indexing comment: {comment.id}")
                SearchIndex.update_comment_index(comment)
                indexed_comment_count += 1

        for chunk in chunked_queryset(
            Post.visible_objects().filter(is_shadow_banned=False)
        ):
            for post in chunk:
                self.stdout.write(f"Indexing post: {post.slug}")
                SearchIndex.update_post_index(post)
                indexed_post_count += 1

        for chunk in chunked_queryset(
            User.objects.filter(moderation_status=User.MODERATION_STATUS_APPROVED)
        ):
            for user in chunk:
                self.stdout.write(f"Indexing user: {user.slug}")
                SearchIndex.update_user_index(user)
                SearchIndex.update_user_tags(user)
                indexed_user_count += 1

        self.stdout.write(
            f"Done 🥙 "
            f"Comments: {indexed_comment_count} Posts: {indexed_post_count} Users: {indexed_user_count}"
        )
Exemple #9
0
def compose_type(request, post_type):
    if post_type not in dict(Post.TYPES):
        raise Http404()

    FormClass = POST_TYPE_MAP.get(post_type) or PostTextForm

    if request.method == "POST":
        form = FormClass(request.POST, request.FILES)
        if form.is_valid():

            if not request.me.is_moderator:
                if Post.check_duplicate(user=request.me,
                                        title=form.cleaned_data["title"]):
                    raise ContentDuplicated()

                is_ok = Post.check_rate_limits(request.me)
                if not is_ok:
                    raise RateLimitException(
                        title="🙅‍♂️ Слишком много постов",
                        message=
                        "В последнее время вы создали слишком много постов. Потерпите, пожалуйста."
                    )

            post = form.save(commit=False)
            post.author = request.me
            post.type = post_type
            post.save()

            PostSubscription.subscribe(request.me, post)

            if post.is_visible:
                if post.topic:
                    post.topic.update_last_activity()

                SearchIndex.update_post_index(post)
                LinkedPost.create_links_from_text(post, post.text)

            if post.is_visible or request.POST.get("show_preview"):
                return redirect("show_post", post.type, post.slug)
            else:
                return redirect("compose")
    else:
        form = FormClass()

    return render(request, f"posts/compose/{post_type}.html", {
        "mode": "create",
        "form": form
    })
Exemple #10
0
def edit_comment(request, comment_id):
    comment = get_object_or_404(Comment, id=comment_id)

    if not request.me.is_moderator:
        if comment.author != request.me:
            raise AccessDenied()

        if comment.is_deleted:
            raise AccessDenied(
                title="Нельзя редактировать удаленный комментарий",
                message="Сначала тот, кто его удалил, должен его восстановить"
            )

        if not comment.is_editable:
            hours = int(settings.COMMENT_EDITABLE_TIMEDELTA.total_seconds() // 3600)
            raise AccessDenied(
                title="Время вышло",
                message=f"Комментарий можно редактировать только в течение {hours} часов после создания"
            )

        if not comment.post.is_visible or not comment.post.is_commentable:
            raise AccessDenied(title="Комментарии к этому посту закрыты")

    post = comment.post

    if request.method == "POST":
        form = CommentForm(request.POST, instance=comment)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.is_deleted = False
            comment.html = None  # flush cache
            comment.ipaddress = parse_ip_address(request)
            comment.useragent = parse_useragent(request)
            comment.save()

            SearchIndex.update_comment_index(comment)

            return redirect("show_comment", post.slug, comment.id)
    else:
        form = CommentForm(instance=comment)

    return render(request, "comments/edit.html", {
        "comment": comment,
        "post": post,
        "form": form
    })
Exemple #11
0
def add_post_index():
	posts = Post.objects.all()
	for post in posts:
		try:
			si = SearchIndex.objects.get(object_id=post.id,object_type='post')
			si.title = post.title
			si.content = post.body
		except:
			si = SearchIndex(
				title = post.title,
				content = post.body,
				object_id = post.id,
				object_type = 'post'
			)
		si.object_image_url = post.author.get_api_profile_image_url()
		si.save()
	print "Indexed ",posts.count()," posts."
Exemple #12
0
def add_course_index():
	courses = Courses.objects.all()
	for course in courses:
		try:
			si = SearchIndex.objects.get(object_id=course.id,object_type='course')
			si.title = course.name
			si.content = course.address1+' , '+course.city+' , '+str(course.zip_code)
			si.object_image_url = mysettings.SITE_URL+"/static/themes/img/default_profile_image_old.jpg"
		except:
			si = SearchIndex(
				title = course.name,
				content = course.address1+' , '+course.city+' , '+str(course.zip_code),
				object_id = course.id,
				object_type = 'course',
				object_image_url = mysettings.SITE_URL+"/static/themes/img/default_profile_image_old.jpg"
			)
		si.save()
	print "Indexed ",courses.count()," courses."
Exemple #13
0
def toggle_tag(request, tag_code):
    if request.method != "POST":
        raise Http404()

    tag = get_object_or_404(Tag, code=tag_code)

    user_tag, is_created = UserTag.objects.get_or_create(
        user=request.me, tag=tag, defaults=dict(name=tag.name)
    )

    if not is_created:
        user_tag.delete()

    SearchIndex.update_user_tags(request.me)

    return {
        "status": "created" if is_created else "deleted",
        "tag": {"code": tag.code, "name": tag.name, "color": tag.color},
    }
Exemple #14
0
def add_profile_index():
	users = GolfUser.objects.all()
	for user in users:
		try:
			si = SearchIndex.objects.get(object_id=user.id,object_type='profile')
			si.title = user.get_full_name()
			si.content = user.email
			si.private = user.is_private
		except:
			si = SearchIndex(
				title = user.get_full_name(),
				content = user.email,
				object_id = user.id,
				object_type = 'profile',
				private = user.is_private,
			)
		si.object_image_url = user.get_api_profile_image_url() 
		si.save()
	print "Indexed ",users.count()," users."
Exemple #15
0
def add_group_index():
	groups = Groups.objects.all()
	for group in groups:
		try:
			si = SearchIndex.objects.get(object_id=group.id,object_type='group')
			si.title = group.name
			si.content = group.description
			si.private = group.is_private
			si.object_image_url = group.get_api_crop_cover_image_url()
		except:
			si = SearchIndex(
				title = group.name,
				content = group.description,
				object_id = group.id,
				object_type = 'group',
				private = group.is_private,
				object_image_url = group.get_api_crop_cover_image_url()
			)
		si.save()
	print "Indexed ",groups.count()," groups."
Exemple #16
0
def add_event_index():
	events = Events.objects.all()
	for event in events:
		try:
			si = SearchIndex.objects.get(object_id=event.id,object_type='event')
			si.title = event.name
			si.content = event.venue+' , '+event.address1+' , '+event.city+' , '+str(event.zip_code)
			si.private = event.is_private
			si.object_image_url = event.get_api_crop_cover_image_url()
		except:
			si = SearchIndex(
				title = event.name,
				content = event.venue+' , '+event.address1+' , '+event.city+' , '+str(event.zip_code),
				object_id = event.id,
				object_type = 'event',
				private = event.is_private,
				object_image_url = event.get_api_crop_cover_image_url()
			)
		si.save()
	print "Indexed ",events.count()," events."
Exemple #17
0
def search(request):
    query = request.GET.get("q")
    if not query:
        return redirect("index")

    results = SearchIndex.search(query).select_related("post", "profile", "comment")

    return render(request, "posts/search.html", {
        "query": query,
        "results": paginate(request, results, page_size=settings.SEARCH_PAGE_SIZE),
    })
Exemple #18
0
def reject_post(update: Update, context: CallbackContext) -> None:
    code, post_id = update.callback_query.data.split(":", 1)
    reason = {
        "reject_post": PostRejectReason.draft,
        "reject_post_title": PostRejectReason.title,
        "reject_post_design": PostRejectReason.design,
        "reject_post_dyor": PostRejectReason.dyor,
        "reject_post_duplicate": PostRejectReason.duplicate,
        "reject_post_chat": PostRejectReason.chat,
        "reject_post_tldr": PostRejectReason.tldr,
        "reject_post_github": PostRejectReason.github,
        "reject_post_bias": PostRejectReason.bias,
        "reject_post_hot": PostRejectReason.hot,
        "reject_post_ad": PostRejectReason.ad,
        "reject_post_inside": PostRejectReason.inside,
        "reject_post_value": PostRejectReason.value,
        "reject_post_draft": PostRejectReason.draft,
        "reject_post_false_dilemma": PostRejectReason.false_dilemma,
    }.get(code) or PostRejectReason.draft

    post = Post.objects.get(id=post_id)
    if not post.is_visible:
        update.effective_chat.send_message(
            f"Пост «{post.title}» уже перенесен в черновики")
        update.callback_query.edit_message_reply_markup(reply_markup=None)
        return None

    post.unpublish()

    SearchIndex.update_post_index(post)

    notify_post_rejected(post, reason)

    update.effective_chat.send_message(
        f"👎 Пост «{post.title}» перенесен в черновики по причине «{reason.value}» ({update.effective_user.full_name})"
    )

    # hide buttons
    update.callback_query.edit_message_reply_markup(reply_markup=None)

    return None
Exemple #19
0
def approve_user_profile(user_id: str, update: Update) -> (str, bool):
    user = User.objects.get(id=user_id)
    if user.is_profile_reviewed and user.is_profile_complete:
        return f"Пользователь «{user.full_name}» уже одобрен", True

    user.is_profile_complete = True
    user.is_profile_reviewed = True
    user.is_profile_rejected = False
    user.save()

    # make intro visible
    Post.objects\
        .filter(author=user, type=Post.TYPE_INTRO)\
        .update(is_visible=True, published_at=datetime.utcnow(), is_approved_by_moderator=True)

    SearchIndex.update_user_index(user)

    notify_user_profile_approved(user)
    send_welcome_drink(user)

    return f"✅ Пользователь «{user.full_name}» одобрен ({update.effective_user.full_name})", True
Exemple #20
0
def edit_comment(request, comment_id):
    comment = get_object_or_404(Comment, id=comment_id)

    if not request.me.is_moderator:
        if comment.author != request.me:
            raise AccessDenied()

        if not comment.is_editable:
            raise AccessDenied(
                title="Время вышло",
                message=
                "Комментарий можно редактировать только в первые 3 часа после создания"
            )

        if not comment.post.is_visible or not comment.post.is_commentable:
            raise AccessDenied(title="Комментарии к этому посту закрыты")

    post = comment.post

    if request.method == "POST":
        form = CommentForm(request.POST, instance=comment)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.is_deleted = False
            comment.html = None  # flush cache
            comment.ipaddress = parse_ip_address(request)
            comment.useragent = parse_useragent(request)
            comment.save()

            SearchIndex.update_comment_index(comment)

            return redirect("show_comment", post.slug, comment.id)
    else:
        form = CommentForm(instance=comment)

    return render(request, "comments/edit.html", {
        "comment": comment,
        "post": post,
        "form": form
    })
Exemple #21
0
def approve_user_profile(user_id: str, update: Update) -> (str, bool):
    user = User.objects.get(id=user_id)
    if user.moderation_status == User.MODERATION_STATUS_APPROVED:
        return f"Пользователь «{user.full_name}» уже одобрен", True

    if user.moderation_status == User.MODERATION_STATUS_REJECTED:
        return f"Пользователь «{user.full_name}» уже отклонен", True

    user.moderation_status = User.MODERATION_STATUS_APPROVED
    user.save()

    # make intro visible
    Post.objects\
        .filter(author=user, type=Post.TYPE_INTRO)\
        .update(is_visible=True, published_at=datetime.utcnow(), is_approved_by_moderator=True)

    SearchIndex.update_user_index(user)

    notify_user_profile_approved(user)
    send_welcome_drink(user)

    return f"✅ Пользователь «{user.full_name}» одобрен ({update.effective_user.full_name})", True
    def handle(self, *args, **options):
        SearchIndex.objects.all().delete()

        for comment in Comment.visible_objects().filter(is_deleted=False,
                                                        post__is_visible=True):
            self.stdout.write(f"Indexing comment: {comment.id}")
            SearchIndex.update_comment_index(comment)

        for post in Post.visible_objects().filter(is_shadow_banned=False):
            self.stdout.write(f"Indexing post: {post.slug}")
            SearchIndex.update_post_index(post)

        for user in User.objects.filter(
                moderation_status=User.MODERATION_STATUS_APPROVED):
            self.stdout.write(f"Indexing user: {user.slug}")
            SearchIndex.update_user_index(user)
            SearchIndex.update_user_tags(user)

        self.stdout.write("Done 🥙")
    def handle(self, *args, **options):
        SearchIndex.objects.all().delete()

        for comment in Comment.visible_objects().filter(is_deleted=False,
                                                        post__is_visible=True):
            self.stdout.write(f"Indexing comment: {comment.id}")
            SearchIndex.update_comment_index(comment)

        for post in Post.visible_objects().filter(is_shadow_banned=False):
            self.stdout.write(f"Indexing post: {post.slug}")
            SearchIndex.update_post_index(post)

        for user in User.objects.filter(is_profile_complete=True,
                                        is_profile_reviewed=True,
                                        is_profile_rejected=False):
            self.stdout.write(f"Indexing user: {user.slug}")
            SearchIndex.update_user_index(user)
            SearchIndex.update_user_tags(user)

        self.stdout.write("Done 🥙")
Exemple #24
0
    def handle(self, *args, **options):
        # render digest using a special html endpoint
        try:
            digest = generate_weekly_digest()
        except NotFound:
            log.error("Weekly digest is empty")
            return

        # get a version without "unsubscribe" footer for posting on home page
        digest_without_footer = generate_weekly_digest(no_footer=True)

        # save digest as a post
        issue = (datetime.utcnow() - settings.LAUNCH_DATE).days // 7
        year, week, _ = (datetime.utcnow() - timedelta(days=7)).isocalendar()
        post, _ = Post.objects.update_or_create(
            slug=f"{year}_{week}",
            type=Post.TYPE_WEEKLY_DIGEST,
            defaults=dict(
                author=User.objects.filter(slug="vas3k").first(),
                title=f"Клубный журнал. Итоги недели. Выпуск #{issue}",
                html=digest_without_footer,
                text=digest_without_footer,
                is_pinned_until=datetime.utcnow() + timedelta(days=1),
                is_visible=True,
                is_public=False,
            ))

        # make it searchable
        SearchIndex.update_post_index(post)

        # sending emails
        subscribed_users = User.objects\
            .filter(
                is_email_verified=True,
                membership_expires_at__gte=datetime.utcnow() - timedelta(days=14),
                moderation_status=User.MODERATION_STATUS_APPROVED,
            )\
            .exclude(email_digest_type=User.EMAIL_DIGEST_TYPE_NOPE)\
            .exclude(is_email_unsubscribed=True)

        for user in subscribed_users:
            self.stdout.write(f"Sending to {user.email}...")

            if not options.get("production") and user.email not in dict(
                    settings.ADMINS).values():
                self.stdout.write(
                    "Test mode. Use --production to send the digest to all users"
                )
                continue

            try:
                digest = digest\
                    .replace("%username%", user.slug)\
                    .replace("%user_id%", str(user.id))\
                    .replace("%secret_code%", base64.b64encode(user.secret_hash.encode("utf-8")).decode())

                send_club_email(
                    recipient=user.email,
                    subject=f"🤘 Клубный журнал. Итоги недели. Выпуск #{issue}",
                    html=digest,
                    tags=["weekly_digest", f"weekly_digest_{issue}"])
            except Exception as ex:
                self.stdout.write(f"Sending to {user.email} failed: {ex}")
                log.exception(f"Error while sending an email to {user.email}")
                continue

        if options.get("production"):
            # flush digest intro and title for next time
            GodSettings.objects.update(digest_intro=None, digest_title=None)

        send_telegram_message(
            chat=CLUB_CHANNEL,
            text=render_html_message("weekly_digest_announce.html", post=post),
            disable_preview=False,
            parse_mode=telegram.ParseMode.HTML,
        )

        self.stdout.write("Done 🥙")
Exemple #25
0
def create_search_index():
    """ Create (or recreate) the search_view materialized view """
    from search.models import SearchIndex
    SearchIndex.create_search_index()
Exemple #26
0
def refresh_search_index():
    """ Update an existing search_view materialized view; will fail if create_search_index hasn't been run once """
    from search.models import SearchIndex
    SearchIndex.refresh_search_index()
Exemple #27
0
    def handle(self, *args, **options):
        # render digest using a special html endpoint
        digest_url = "https://vas3k.club" + reverse("render_weekly_digest")
        self.stdout.write(f"Generating digest: {digest_url}")

        digest_html_response = requests.get(digest_url)
        if digest_html_response.status_code > 400:
            log.error("Weekly digest error: bad status code", extra={"html": digest_html_response.text})
            return

        digest_html = digest_html_response.text

        # save digest as a post
        issue = (datetime.utcnow() - settings.LAUNCH_DATE).days // 7
        year, week, _ = (datetime.utcnow() - timedelta(days=7)).isocalendar()
        post, _ = Post.objects.update_or_create(
            slug=f"{year}_{week}",
            type=Post.TYPE_WEEKLY_DIGEST,
            defaults=dict(
                author=User.objects.filter(slug="vas3k").first(),
                title=f"Клубный журнал. Итоги недели. Выпуск #{issue}",
                html=digest_html,
                text=digest_html,
                is_pinned_until=datetime.utcnow() + timedelta(days=1),
                is_visible=True,
                is_public=False,
            )
        )

        SearchIndex.update_post_index(post)

        # sending emails
        subscribed_users = User.objects\
            .filter(
                is_email_verified=True,
                membership_expires_at__gte=datetime.utcnow() - timedelta(days=14)
            )\
            .exclude(email_digest_type=User.EMAIL_DIGEST_TYPE_NOPE)\
            .exclude(is_profile_rejected=True)\
            .exclude(is_email_unsubscribed=True)

        for user in subscribed_users:
            self.stdout.write(f"Sending to {user.email}...")

            if not options.get("production") and user.email != "*****@*****.**":
                self.stdout.write("Test mode. Use --production to send the digest to all users")
                continue

            try:
                user_digest_html = str(digest_html)
                user_digest_html = user_digest_html\
                    .replace("%username%", user.slug)\
                    .replace("%user_id%", str(user.id))\
                    .replace("%secret_code%", user.secret_hash)

                send_club_email(
                    recipient=user.email,
                    subject=f"🤘 Клубный журнал. Итоги недели. Выпуск #{issue}",
                    html=user_digest_html,
                    tags=["weekly_digest", f"weekly_digest_{issue}"]
                )
            except Exception as ex:
                self.stdout.write(f"Sending to {user.email} failed: {ex}")
                log.exception(f"Error while sending an email to {user.email}")
                continue

        if options.get("production"):
            # flush digest intro for next time
            GodSettings.objects.update(digest_intro=None)

        send_telegram_message(
            chat=CLUB_CHANNEL,
            text=render_html_message("weekly_digest_announce.html", post=post),
            disable_preview=False,
            parse_mode=telegram.ParseMode.HTML,
        )

        self.stdout.write("Done 🥙")