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, })
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 })
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
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}" )
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 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 })
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) 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( 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 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
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 🥙")
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 🥙")