Skip to content

Commit c454076

Browse files
authored
Merge pull request #9167 from Couchers-org/web/bugfix/incomplete-profile-discussion-block
Block incomplete profiles from creating discussions, tested and verified
2 parents 52d3c22 + 89d3cbf commit c454076

7 files changed

Lines changed: 66 additions & 7 deletions

File tree

app/backend/src/couchers/i18n/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"host_request_too_short2_one": "Host request cannot be shorter than {{count}} character.",
9696
"host_request_too_short2_other": "Host request cannot be shorter than {{count}} characters.",
9797
"hosting_status_required": "Hosting status is required.",
98+
"incomplete_profile_create_discussion": "You have to complete your profile before you can start a discussion.",
9899
"incomplete_profile_create_event": "You have to complete your profile before you can create an event.",
99100
"incomplete_profile_create_public_trip": "You have to complete your profile before you can create a public trip.",
100101
"incomplete_profile_send_friend_request": "You have to complete your profile before you can send a friend request.",

app/backend/src/couchers/servicers/discussions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from couchers.context import CouchersContext, make_notification_user_context
99
from couchers.db import can_moderate_node, session_scope
1010
from couchers.event_log import log_event
11+
from couchers.helpers.completed_profile import has_completed_profile
1112
from couchers.jobs.enqueue import queue_job
1213
from couchers.models import Cluster, ClusterSubscription, Discussion, ModerationObjectType, Thread, User
1314
from couchers.models.discussions import ContentChangeType, DiscussionVersion
@@ -119,6 +120,10 @@ def CreateDiscussion(
119120
if not cluster.small_community_features_enabled:
120121
context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "cannot_create_discussion")
121122

123+
user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
124+
if not has_completed_profile(session, user):
125+
context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "incomplete_profile_create_discussion")
126+
122127
thread = Thread()
123128
session.add(thread)
124129
session.flush()

app/backend/src/tests/test_discussions.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,3 +861,23 @@ def test_admin_delete_discussion_creates_version_record(db):
861861
assert v.old_title == "Admin will delete this"
862862
assert v.new_title is None
863863
assert v.editor_user_id == admin.id
864+
865+
866+
def test_create_discussion_incomplete_profile(db):
867+
user, token = generate_user(complete_profile=False)
868+
user2, token2 = generate_user()
869+
870+
with session_scope() as session:
871+
community_id = create_community(session, 0, 1, "Testing Community", [user2], [], None).id
872+
873+
with discussions_session(token) as api:
874+
with pytest.raises(grpc.RpcError) as e:
875+
api.CreateDiscussion(
876+
discussions_pb2.CreateDiscussionReq(
877+
title="dummy title",
878+
content="dummy content",
879+
owner_community_id=community_id,
880+
)
881+
)
882+
assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
883+
assert e.value.details() == "You have to complete your profile before you can start a discussion."

app/web/components/ProfileIncompleteDialog/ProfileIncompleteDialog.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ interface ProfileIncompleteDialogProps {
2121
| "send_message"
2222
| "send_request"
2323
| "create_public_trip"
24-
| "send_friend_request";
24+
| "send_friend_request"
25+
| "create_discussion";
2526
}
2627

2728
export default function ProfileIncompleteDialog({
@@ -60,10 +61,18 @@ export default function ProfileIncompleteDialog({
6061
</DialogContentText>
6162
</DialogContent>
6263
<DialogActions>
63-
<Button variant="outlined" onClick={onClose}>
64+
<Button
65+
variant="outlined"
66+
onClick={onClose}
67+
sx={{ textAlign: "center" }}
68+
>
6469
{t("profile:complete_profile_dialog.cancel_button")}
6570
</Button>
66-
<Button component={Link} href={routeToEditProfile()}>
71+
<Button
72+
component={Link}
73+
href={routeToEditProfile()}
74+
sx={{ textAlign: "center" }}
75+
>
6776
{t("profile:complete_profile_dialog.edit_profile_button")}
6877
</Button>
6978
</DialogActions>

app/web/features/communities/discussions/DiscussionsListPage.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import community from "test/fixtures/community.json";
1010
import discussions from "test/fixtures/discussions.json";
1111
import wrapper from "test/hookWrapper";
1212
import i18n from "test/i18n";
13-
import { getLiteUser } from "test/serviceMockDefaults";
13+
import { getAccountInfo, getLiteUser } from "test/serviceMockDefaults";
1414
import { assertErrorAlert, mockConsoleError, MockedService } from "test/utils";
1515

1616
import { DISCUSSION_CARD_TEST_ID } from "./DiscussionCard";
@@ -23,6 +23,9 @@ jest.mock("components/MarkdownInput");
2323
const getLiteUserMock = service.user.getLiteUser as MockedService<
2424
typeof service.user.getLiteUser
2525
>;
26+
const getAccountInfoMock = service.account.getAccountInfo as MockedService<
27+
typeof service.account.getAccountInfo
28+
>;
2629
const createDiscussionMock = service.discussions
2730
.createDiscussion as MockedService<
2831
typeof service.discussions.createDiscussion
@@ -33,6 +36,7 @@ const listDiscussionsMock = service.communities
3336
describe("DiscussionsListPage", () => {
3437
beforeEach(() => {
3538
getLiteUserMock.mockImplementation(getLiteUser);
39+
getAccountInfoMock.mockImplementation(getAccountInfo);
3640
listDiscussionsMock.mockResolvedValue({
3741
discussionsList: discussions,
3842
nextPageToken: "",

app/web/features/communities/discussions/DiscussionsListPage.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import Alert from "components/Alert";
33
import Button from "components/Button";
44
import CenteredSpinner from "components/CenteredSpinner/CenteredSpinner";
55
import { EmailIcon } from "components/Icons";
6+
import ProfileIncompleteDialog from "components/ProfileIncompleteDialog/ProfileIncompleteDialog";
67
import TextBody from "components/TextBody";
8+
import useAccountInfo from "features/auth/useAccountInfo";
79
import { SectionTitle } from "features/communities/CommunityPage";
810
import { useListDiscussions } from "features/communities/hooks";
911
import { useTranslation } from "i18n";
@@ -62,10 +64,13 @@ export default function DiscussionsListPage({
6264
}) {
6365
const { t } = useTranslation([COMMUNITIES]);
6466

67+
const { data: accountInfo } = useAccountInfo();
6568
const hash = typeof window !== "undefined" ? window.location.hash : "";
69+
// Intent to create a post, set by both the #new hash and the "New post" button.
6670
const [isCreatingNewPost, setIsCreatingNewPost] = useState(
6771
hash.includes("new"),
6872
);
73+
6974
const {
7075
isLoading: isDiscussionsLoading,
7176
isFetching: isDiscussionsFetching,
@@ -78,8 +83,22 @@ export default function DiscussionsListPage({
7883
// loading is false when refetched since there's old data in cache already
7984
const isRefetching = !isDiscussionsLoading && isDiscussionsFetching;
8085

86+
// Derive form/dialog visibility during render so both entry points share the
87+
// same gate: show the form only once the profile is known to be complete,
88+
// otherwise show the dialog.
89+
const profileIncomplete =
90+
accountInfo !== undefined && !accountInfo.profileComplete;
91+
const showCreateForm =
92+
isCreatingNewPost && accountInfo?.profileComplete === true;
93+
const profileDialogOpen = isCreatingNewPost && profileIncomplete;
94+
8195
return (
8296
<>
97+
<ProfileIncompleteDialog
98+
open={profileDialogOpen}
99+
onClose={() => setIsCreatingNewPost(false)}
100+
attempted_action="create_discussion"
101+
/>
83102
<StyledDiscussionsHeader>
84103
<SectionTitle icon={<EmailIcon />}>
85104
{t("communities:discussions_title")}
@@ -88,7 +107,7 @@ export default function DiscussionsListPage({
88107
{discussionsError && (
89108
<Alert severity="error">{discussionsError.message}</Alert>
90109
)}
91-
<Collapse in={!isCreatingNewPost}>
110+
<Collapse in={!showCreateForm}>
92111
<StyledNewPostButtonContainer>
93112
<StyledCreateResourceButton
94113
onClick={() => setIsCreatingNewPost(true)}
@@ -98,7 +117,7 @@ export default function DiscussionsListPage({
98117
{isRefetching && <CenteredSpinner />}
99118
</StyledNewPostButtonContainer>
100119
</Collapse>
101-
<Collapse in={isCreatingNewPost}>
120+
<Collapse in={showCreateForm}>
102121
<CreateDiscussionForm
103122
communityId={community.communityId}
104123
onCancel={() => setIsCreatingNewPost(false)}

app/web/features/profile/locales/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,8 @@
341341
"send_message": "send a message",
342342
"send_friend_request": "send a friend request",
343343
"create_event": "create an event",
344-
"create_public_trip": "create a public trip"
344+
"create_public_trip": "create a public trip",
345+
"create_discussion": "create a discussion"
345346
},
346347
"edit_profile_button": "Edit your profile now",
347348
"cancel_button": "Never mind"

0 commit comments

Comments
 (0)