forked from vas3k/vas3k.club
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Badges draft * Badges CSS for comments, posts, profiles * Almost done, needs data and icons * CSS fixes + frontend bugs * More logical fixes * Fixing bugs, rename things * Add icons and initial data
- Loading branch information
Showing
70 changed files
with
1,210 additions
and
95 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class BadgesConfig(AppConfig): | ||
default_auto_field = "django.db.models.BigAutoField" | ||
name = "badges" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# Generated by Django 3.2.5 on 2021-10-20 10:42 | ||
|
||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
import uuid | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
('users', '0022_alter_user_secret_hash'), | ||
('comments', '0008_auto_20210911_0827'), | ||
('posts', '0024_postsubscription_type'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='Badge', | ||
fields=[ | ||
('code', models.CharField(max_length=32, primary_key=True, serialize=False, unique=True)), | ||
('created_at', models.DateTimeField(auto_now_add=True)), | ||
('title', models.CharField(max_length=64)), | ||
('description', models.CharField(max_length=256, null=True)), | ||
('price_days', models.IntegerField(default=10)), | ||
('is_visible', models.BooleanField(default=True)), | ||
], | ||
options={ | ||
'db_table': 'badges', | ||
}, | ||
), | ||
migrations.CreateModel( | ||
name='UserBadge', | ||
fields=[ | ||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), | ||
('created_at', models.DateTimeField(auto_now_add=True)), | ||
('note', models.TextField(null=True)), | ||
('badge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_badges', to='badges.badge')), | ||
('comment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='comment_badges', to='comments.comment')), | ||
('from_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='from_badges', to='users.user')), | ||
('post', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='post_badges', to='posts.post')), | ||
('to_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='to_badges', to='users.user')), | ||
], | ||
options={ | ||
'db_table': 'user_badges', | ||
'unique_together': {('from_user', 'to_user', 'badge', 'comment_id'), ('from_user', 'to_user', 'badge', 'post_id')}, | ||
}, | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# Generated by Django 3.2.5 on 2021-10-22 08:58 | ||
|
||
from django.db import migrations | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('badges', '0001_initial'), | ||
] | ||
|
||
operations = [ | ||
migrations.RunSQL(""" | ||
INSERT INTO "badges"("code","created_at","title","description","price_days","is_visible") | ||
VALUES | ||
('bad','2021-10-15 11:37:53.514167+02','Так плохо, что даже хорошо',NULL,110,FALSE), | ||
('bmw','2021-10-15 11:37:53.514167+02','Беха','Выдать автору беху по программе помощи миллионерам',150,TRUE), | ||
('cool','2021-10-15 11:37:53.514167+02','ОХУЕННО','Вот такой контент я хочу видеть в Клубе!',40,TRUE), | ||
('cry','2021-10-15 11:37:53.514167+02','Я не плачу...','...это дождь',80,TRUE), | ||
('dolor','2021-10-15 11:37:53.514167+02','Держи долор','Его заберёт налоговая, но нам останется ХоРоШеЕ НаСтРоЕнИе!',30,TRUE), | ||
('hug','2021-10-15 11:37:53.514167+02','Дай обниму',NULL,60,TRUE), | ||
('insight','2021-10-15 11:37:53.514167+02','Я понял!','Моя жизнь больше не будет прежней',50,TRUE), | ||
('lov','2021-10-15 11:37:53.514167+02','Супер-любовь','За восстановление веры в человечество',80,TRUE), | ||
('faang','2021-10-15 11:37:53.514167+02','Оффер в FAANG','Я бы тебя нанял',200,TRUE), | ||
('year','2021-10-15 11:37:53.514167+02','Пост Года','Лучшее, что я читал в этом году',365,TRUE), | ||
('lol','2021-10-15 11:37:53.514167+02','Как же я ору',NULL,60,TRUE); | ||
""") | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import math | ||
from datetime import timedelta | ||
from uuid import uuid4 | ||
|
||
from django.db import models, transaction, IntegrityError | ||
from django.db.models import F, Count | ||
|
||
from club.exceptions import InsufficientFunds, BadRequest, ContentDuplicated | ||
from comments.models import Comment | ||
from posts.models.post import Post | ||
from users.models.user import User | ||
|
||
|
||
class Badge(models.Model): | ||
code = models.CharField(primary_key=True, max_length=32, null=False, unique=True) | ||
created_at = models.DateTimeField(auto_now_add=True) | ||
|
||
title = models.CharField(max_length=64, null=False) | ||
description = models.CharField(max_length=256, null=True) | ||
price_days = models.IntegerField(default=10) | ||
is_visible = models.BooleanField(default=True) | ||
|
||
class Meta: | ||
db_table = "badges" | ||
ordering = ["price_days", "code"] | ||
|
||
@classmethod | ||
def visible_objects(cls): | ||
return cls.objects.filter(is_visible=True) | ||
|
||
|
||
class UserBadge(models.Model): | ||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False) | ||
|
||
badge = models.ForeignKey(Badge, related_name="user_badges", on_delete=models.CASCADE) | ||
created_at = models.DateTimeField(auto_now_add=True) | ||
|
||
from_user = models.ForeignKey(User, related_name="from_badges", null=True, on_delete=models.SET_NULL) | ||
to_user = models.ForeignKey(User, related_name="to_badges", on_delete=models.CASCADE) | ||
post = models.ForeignKey(Post, related_name="post_badges", null=True, on_delete=models.SET_NULL) | ||
comment = models.ForeignKey(Comment, related_name="comment_badges", null=True, on_delete=models.SET_NULL) | ||
|
||
note = models.TextField(null=True) | ||
|
||
class Meta: | ||
db_table = "user_badges" | ||
unique_together = [ | ||
("from_user", "to_user", "badge", "post_id"), | ||
("from_user", "to_user", "badge", "comment_id"), | ||
] | ||
|
||
@classmethod | ||
def create_user_badge(cls, badge, from_user, to_user, post=None, comment=None, note=None): | ||
if from_user == to_user: | ||
raise BadRequest( | ||
title="🛑 Нельзя дарить награды самому себе", | ||
message="Это что такое-то вообще!" | ||
) | ||
|
||
if badge.price_days >= from_user.membership_days_left(): | ||
raise InsufficientFunds( | ||
title="💸 Недостаточно средств :(", | ||
message=f"Вы не можете подарить юзеру эту награду, " | ||
f"так как у вас осталось {math.floor(from_user.membership_days_left())} дней членства, " | ||
f"а награда стоит {badge.price_days}. " | ||
f"Продлите членство в настройках своего профиля." | ||
) | ||
|
||
with transaction.atomic(): | ||
# store user badge | ||
try: | ||
user_badge = UserBadge.objects.create( | ||
badge=badge, | ||
from_user=from_user, | ||
to_user=to_user, | ||
post=post, | ||
comment=comment, | ||
note=note, | ||
) | ||
except IntegrityError: | ||
raise ContentDuplicated( | ||
title="🛑 Вы уже дарили награду за этот пост или комментарий", | ||
message="Повторно награды дарить нельзя. Но вы можете подарить другую награду." | ||
) | ||
|
||
# deduct days balance from profile | ||
User.objects\ | ||
.filter(id=from_user.id)\ | ||
.update( | ||
membership_expires_at=F("membership_expires_at") - timedelta(days=badge.price_days) | ||
) | ||
|
||
# add badge to post/comment metadata (just for caching) | ||
comment_or_post = comment or post | ||
metadata = comment_or_post.metadata or {} | ||
badges = metadata.get("badges") or {} | ||
if badge.code not in badges: | ||
# add new badge | ||
badges[badge.code] = { | ||
"title": badge.title, | ||
"description": badge.description, | ||
"count": 1, | ||
} | ||
else: | ||
# increment badge count for this post | ||
badges[badge.code]["count"] += 1 | ||
|
||
# update only that metadata (do not use .save(), it saves all fields and can cause side-effects) | ||
metadata["badges"] = badges | ||
type(comment_or_post).objects.filter(id=comment_or_post.id).update(metadata=metadata) | ||
|
||
return user_badge | ||
|
||
@classmethod | ||
def user_badges(cls, user): | ||
return UserBadge.objects.filter(to_user=user).select_related("badge").order_by("-created_at") | ||
|
||
@classmethod | ||
def user_badges_grouped(cls, user): | ||
badges = { | ||
badge.code: badge for badge in Badge.visible_objects() | ||
} | ||
|
||
badge_groups = UserBadge.objects\ | ||
.filter(to_user=user)\ | ||
.order_by("badge_id")\ | ||
.values("badge_id")\ | ||
.annotate(count=Count("badge_id")) | ||
|
||
return { | ||
badge_group["badge_id"]: { | ||
"title": badges[badge_group["badge_id"]].title, | ||
"description": badges[badge_group["badge_id"]].description, | ||
"count": badge_group["count"], | ||
} for badge_group in badge_groups | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
from django.conf import settings | ||
from django.shortcuts import get_object_or_404, render | ||
|
||
from auth.helpers import auth_required | ||
from badges.models import Badge, UserBadge | ||
from club.exceptions import BadRequest | ||
from comments.models import Comment | ||
from posts.models.post import Post | ||
|
||
|
||
@auth_required | ||
def create_badge_for_post(request, post_slug): | ||
post = get_object_or_404(Post, slug=post_slug) | ||
if post.deleted_at: | ||
raise BadRequest( | ||
title="😵 Пост удалён", | ||
message="Нельзя давать награды за удалённые посты" | ||
) | ||
|
||
if request.method != "POST": | ||
if request.me.membership_days_left() < settings.MIN_DAYS_TO_GIVE_BADGES: | ||
return render(request, "badges/messages/insufficient_funds.html") | ||
|
||
return render(request, "badges/create.html", { | ||
"post": post, | ||
"badges": Badge.visible_objects().all(), | ||
}) | ||
|
||
badge_code = request.POST.get("badge_code") | ||
badge = Badge.objects.filter(code=badge_code).first() | ||
if not badge or not badge.is_visible: | ||
raise BadRequest( | ||
title="🙅♀️ Бейджик недоступен", | ||
message="Данную награду пока нельзя выдавать" | ||
) | ||
|
||
note = (request.POST.get("note") or "")[:1000] | ||
user_badge = UserBadge.create_user_badge( | ||
badge=badge, | ||
from_user=request.me, | ||
to_user=post.author, | ||
post=post, | ||
note=note, | ||
) | ||
|
||
return render(request, "badges/messages/success.html", { | ||
"user_badge": user_badge, | ||
"show_funds_warning": request.me.membership_days_left() - user_badge.badge.price_days < settings.MIN_DAYS_TO_GIVE_BADGES, | ||
}) | ||
|
||
|
||
@auth_required | ||
def create_badge_for_comment(request, comment_id): | ||
comment = get_object_or_404(Comment, id=comment_id) | ||
if comment.is_deleted: | ||
raise BadRequest( | ||
title="😵 Комментарий удалён", | ||
message="Нельзя выдавать награды за удалённые комменты" | ||
) | ||
|
||
if request.method != "POST": | ||
if request.me.membership_days_left() < settings.MIN_DAYS_TO_GIVE_BADGES: | ||
return render(request, "badges/messages/insufficient_funds.html") | ||
|
||
return render(request, "badges/create.html", { | ||
"comment": comment, | ||
"badges": Badge.visible_objects().all(), | ||
}) | ||
|
||
badge_code = request.POST.get("badge_code") | ||
badge = Badge.objects.filter(code=badge_code).first() | ||
if not badge or not badge.is_visible: | ||
raise BadRequest( | ||
title="🙅♀️ Бейджик недоступен", | ||
message="Данную награду пока нельзя выдавать" | ||
) | ||
|
||
note = (request.POST.get("note") or "")[:1000] | ||
user_badge = UserBadge.create_user_badge( | ||
badge=badge, | ||
from_user=request.me, | ||
to_user=comment.author, | ||
comment=comment, | ||
note=note, | ||
) | ||
|
||
return render(request, "badges/messages/success.html", { | ||
"user_badge": user_badge, | ||
"show_funds_warning": request.me.membership_days_left() - user_badge.badge.price_days < settings.MIN_DAYS_TO_GIVE_BADGES, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.