Skip to content
This repository was archived by the owner on Feb 16, 2023. It is now read-only.

Commit 685698f

Browse files
committed
[27][28] Add paste rating
1 parent 6057a21 commit 685698f

File tree

12 files changed

+357
-233
lines changed

12 files changed

+357
-233
lines changed

.gitignore

+203-202
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 4.0.3 on 2022-03-30 17:01
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('wklejki', '0005_alter_paste_created_at'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='paste',
16+
name='likers',
17+
field=models.ManyToManyField(blank=True, related_name='liked_pastes', to=settings.AUTH_USER_MODEL),
18+
),
19+
]

Backend/wklejki/models.py

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class Paste(models.Model):
2222
author = models.ForeignKey(
2323
CustomUser, on_delete=models.CASCADE, related_name="pastes"
2424
)
25+
likers = models.ManyToManyField(CustomUser, related_name="liked_pastes", blank=True)
26+
2527
title = models.CharField(max_length=30)
2628
content = models.TextField()
2729
created_at = models.DateTimeField(auto_now_add=True, db_index=True)

Backend/wklejki/schema.py

+51-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Standard Library
22
import logging
3-
from typing import Optional
43
import re
4+
from typing import Optional
55

66
# Django
77
from django.contrib.auth import get_user_model
@@ -38,7 +38,7 @@ class Meta:
3838
take=graphene.Int(description="Take n items when paginating"),
3939
)
4040

41-
paste_count = graphene.Int(description="Total number of pastes")
41+
paste_count = graphene.Int(description="Total number of pastes for this user")
4242

4343
@gql_optimizer.resolver_hints(model_field='pastes')
4444
def resolve_pastes(
@@ -141,7 +141,9 @@ def mutate(
141141
raise Exception("Username contains restricted special characters")
142142
if not re.match("^[A-Za-z0-9._%+-]*$", password):
143143
raise Exception("Password contains restricted special characters")
144-
if not re.fullmatch(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', email):
144+
if not re.fullmatch(
145+
r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', email
146+
):
145147
raise Exception("Enter a valid e-mail")
146148
user = get_user_model()(
147149
username=username,
@@ -178,7 +180,9 @@ def mutate(
178180
user = get_user_model().objects.get(pk=id)
179181
if not re.match("^[A-Za-z0-9._%+-]*$", username):
180182
raise Exception("Username contains restricted special characters")
181-
if not re.fullmatch(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', email):
183+
if not re.fullmatch(
184+
r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', email
185+
):
182186
raise Exception("Enter a valid e-mail")
183187
if username is not None:
184188
user.username = username
@@ -226,7 +230,16 @@ class Meta:
226230
model = Paste
227231

228232
id = graphene.Int()
229-
paste_count = graphene.Int(description="Total number of pastes")
233+
like_count = graphene.Int(description="Number of users who like this paste")
234+
is_liked = graphene.Boolean(description="Does the current user like this paste?")
235+
236+
def resolve_like_count(self: Paste, info: ResolveInfo) -> int:
237+
return self.likers.count()
238+
239+
def resolve_is_liked(self: Paste, info: ResolveInfo) -> bool:
240+
if info.context.user.is_authenticated:
241+
return self.likers.filter(pk=info.context.user.pk).exists()
242+
return False
230243

231244

232245
class PasteQuery(graphene.ObjectType):
@@ -377,10 +390,43 @@ def mutate(self, info: ResolveInfo, id: int) -> "DeletePaste":
377390
return DeletePaste(ok=True)
378391

379392

393+
class LikePaste(graphene.Mutation):
394+
"""Likes or unlikes a paste"""
395+
396+
paste = graphene.Field(PasteType)
397+
398+
class Arguments:
399+
id = graphene.Int(required=True)
400+
liking = graphene.Boolean(required=True)
401+
402+
@login_required
403+
def mutate(self, info: ResolveInfo, id: int, liking: bool) -> Paste:
404+
paste: Paste = Paste.objects.get(pk=id)
405+
406+
if paste.private and info.context.user != paste.author:
407+
raise Paste.DoesNotExist("Paste matching query does not exist")
408+
409+
if liking:
410+
paste.likers.add(info.context.user)
411+
logging.info(
412+
f"User '{info.context.user}' liked paste '{paste}'"
413+
f"by user '{paste.author}' :)"
414+
)
415+
else:
416+
paste.likers.remove(info.context.user)
417+
logging.info(
418+
f"User '{info.context.user}' unliked paste '{paste}'"
419+
f"by user '{paste.author}' :("
420+
)
421+
422+
return LikePaste(paste=paste)
423+
424+
380425
class PasteMutation(graphene.ObjectType):
381426
create_paste = CreatePaste.Field()
382427
update_paste = UpdatePaste.Field()
383428
delete_paste = DeletePaste.Field()
429+
like_paste = LikePaste.Field()
384430

385431

386432
class Query(UserQuery, PasteQuery, graphene.ObjectType):

Frontend/pz-ip-app/src/App.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
border-color: #282c34 !important;
9292
}
9393

94-
.heart-button {
94+
.paste-like-button {
9595
padding: 0;
9696
border: none;
9797
background-color: transparent;

Frontend/pz-ip-app/src/Components/Paste/PasteLists/Base/PasteList.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const PasteList = (props: Props) => {
4141
<th className="text-muted col-2">Utworzona</th>
4242
<th className="text-muted col-2">Zmieniona</th>
4343
<th className="text-muted col-1">Autor</th>
44+
<th className="text-muted col-1">Lajki</th>
4445
<th className="text-muted col-1 text-center">Prywatna</th>
4546
<th className="text-muted col-1"></th>
4647
</tr>

Frontend/pz-ip-app/src/Components/Paste/PasteLists/Base/PasteRow.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ const PasteRow = ({ paste, refetch }: Props) => {
6363
{paste.author.username}
6464
</td>
6565

66+
<td className="text-muted" style={{ verticalAlign: "middle" }}>
67+
{paste.likeCount}
68+
</td>
69+
6670
<td
6771
className="text-muted"
6872
style={{ verticalAlign: "middle", textAlign: "center" }}

Frontend/pz-ip-app/src/Components/Paste/Types.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ export interface PasteInfo {
55
createdAt: Date;
66
updatedAt: Date;
77
isPrivate: boolean;
8+
likeCount: number;
9+
isLiked: boolean;
810
}

Frontend/pz-ip-app/src/Components/Paste/ViewEditPaste.tsx

+13-7
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,13 @@ const ViewEditPaste = ({ id }: Props) => {
7777
}
7878
}
7979

80-
useQuery(get_paste, {
80+
const { data } = useQuery(get_paste, {
8181
variables: { id },
82-
onCompleted: (data) => {
83-
setPasteTitle(data.paste.title);
84-
setPasteContent(data.paste.content);
85-
setPasteAuthor(data.paste.author);
86-
setPasteIsPrivate(data.paste.isPrivate);
82+
onCompleted: (cdata) => {
83+
setPasteTitle(cdata.paste.title);
84+
setPasteContent(cdata.paste.content);
85+
setPasteAuthor(cdata.paste.author);
86+
setPasteIsPrivate(cdata.paste.isPrivate);
8787
},
8888
});
8989

@@ -109,7 +109,13 @@ const ViewEditPaste = ({ id }: Props) => {
109109
{error === "" ? null : (
110110
<div className="alert alert-danger mt-3 text-center">{error}</div>
111111
)}
112-
{!isEditing && <Rate />}
112+
{!isEditing && data && (
113+
<Rate
114+
id={data.paste.id}
115+
pasteLikes={data.paste.likeCount}
116+
liking={data.paste.isLiked}
117+
/>
118+
)}
113119
<RenderPaste
114120
editable={isEditing}
115121
title={pasteTitle}

Frontend/pz-ip-app/src/Components/Rating/Rate.tsx

+37-15
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,50 @@
11
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
22
import { solid } from "@fortawesome/fontawesome-svg-core/import.macro";
33
import { useState } from "react";
4-
import { Stack, Button } from "react-bootstrap";
4+
import { useMutation } from "@apollo/client";
5+
import { like_paste } from "../../Queries/queries";
6+
7+
interface Props {
8+
id: number;
9+
pasteLikes: number;
10+
liking: boolean;
11+
}
12+
13+
const Rate = ({ id, pasteLikes, liking }: Props) => {
14+
const [isLiking, setLiking] = useState(liking);
15+
const [likeCount, setLikeCount] = useState(pasteLikes);
16+
const heartColor: string = isLiking ? "red" : "grey";
17+
18+
const saved_state = { liking, likeCount };
19+
20+
const [doLike] = useMutation(like_paste, {
21+
onError: (error) => {
22+
console.log(error);
23+
setLiking(saved_state.liking);
24+
setLikeCount(saved_state.likeCount);
25+
},
26+
});
527

6-
const Rate = () => {
7-
const [like, setLike] = useState(false);
8-
const [likeCount, setLikeCount] = useState(1233);
9-
const heartColor: string = like ? "red" : "grey";
1028
function likeHandler() {
11-
setLike(!like);
12-
{
13-
like ? setLikeCount(likeCount - 1) : setLikeCount(likeCount + 1);
29+
if (isLiking) {
30+
setLikeCount(likeCount - 1);
31+
} else {
32+
setLikeCount(likeCount + 1);
1433
}
34+
35+
setLiking(!isLiking);
36+
doLike({
37+
variables: {
38+
id,
39+
liking: !isLiking,
40+
},
41+
});
1542
}
43+
1644
return (
1745
<span className="float-end mt-5 pt-1 pb-1">
1846
<span className="align-middle">{likeCount}</span>
19-
<button
20-
//variant="link"
21-
className="heart-button align-middle"
22-
onClick={() => {
23-
likeHandler();
24-
}}
25-
>
47+
<button className="paste-like-button align-middle" onClick={likeHandler}>
2648
<FontAwesomeIcon
2749
style={{ fontSize: "30px", color: heartColor }}
2850
icon={solid("heart")}

Frontend/pz-ip-app/src/Queries/queries.tsx

+21
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ export const get_paste = gql`
8585
title
8686
content
8787
isPrivate: private
88+
isLiked
89+
likeCount
8890
author {
8991
id
9092
username
@@ -108,6 +110,9 @@ export const update_paste = gql`
108110
) {
109111
paste {
110112
id
113+
title
114+
content
115+
private
111116
}
112117
}
113118
}
@@ -131,6 +136,18 @@ export const delete_paste = gql`
131136
}
132137
`;
133138

139+
export const like_paste = gql`
140+
mutation ($id: Int!, $liking: Boolean!) {
141+
likePaste(id: $id, liking: $liking) {
142+
paste {
143+
id
144+
isLiked
145+
likeCount
146+
}
147+
}
148+
}
149+
`;
150+
134151
export const get_pastes = gql`
135152
query ($skip: Int, $take: Int) {
136153
pasteCount
@@ -155,6 +172,8 @@ export const get_paste_titles = gql`
155172
title
156173
createdAt
157174
updatedAt
175+
likeCount
176+
isLiked
158177
isPrivate: private
159178
author {
160179
id
@@ -175,6 +194,8 @@ export const get_paste_titles_for_user = gql`
175194
createdAt
176195
updatedAt
177196
isPrivate: private
197+
likeCount
198+
isLiked
178199
author {
179200
id
180201
username

Frontend/pz-ip-app/src/index.css

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
@import url("https://fonts.googleapis.com/css2?family=Lato&display=swap");
2+
13
body {
24
margin: 0;
3-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4-
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5-
sans-serif;
5+
font-family: "Lato", sans-serif;
66
-webkit-font-smoothing: antialiased;
77
-moz-osx-font-smoothing: grayscale;
88
}

0 commit comments

Comments
 (0)