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 })
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 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
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 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()
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}" )
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 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 })
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."
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."
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}, }
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."
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."
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."
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), })
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 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
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 })
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 🥙")
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 🥙")
def create_search_index(): """ Create (or recreate) the search_view materialized view """ from search.models import SearchIndex SearchIndex.create_search_index()
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()
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 🥙")