Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/backend/src/couchers/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"host_request_too_short2_one": "Host request cannot be shorter than {{count}} character.",
"host_request_too_short2_other": "Host request cannot be shorter than {{count}} characters.",
"hosting_status_required": "Hosting status is required.",
"incomplete_profile_create_discussion": "You have to complete your profile before you can start a discussion.",
"incomplete_profile_create_event": "You have to complete your profile before you can create an event.",
"incomplete_profile_create_public_trip": "You have to complete your profile before you can create a public trip.",
"incomplete_profile_send_friend_request": "You have to complete your profile before you can send a friend request.",
Expand Down
5 changes: 5 additions & 0 deletions app/backend/src/couchers/servicers/discussions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from couchers.context import CouchersContext, make_notification_user_context
from couchers.db import can_moderate_node, session_scope
from couchers.event_log import log_event
from couchers.helpers.completed_profile import has_completed_profile
from couchers.jobs.enqueue import queue_job
from couchers.models import Cluster, ClusterSubscription, Discussion, ModerationObjectType, Thread, User
from couchers.models.discussions import ContentChangeType, DiscussionVersion
Expand Down Expand Up @@ -119,6 +120,10 @@ def CreateDiscussion(
if not cluster.small_community_features_enabled:
context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "cannot_create_discussion")

user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
if not has_completed_profile(session, user):
context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "incomplete_profile_create_discussion")

thread = Thread()
session.add(thread)
session.flush()
Expand Down
20 changes: 20 additions & 0 deletions app/backend/src/tests/test_discussions.py
Original file line number Diff line number Diff line change
Expand Up @@ -861,3 +861,23 @@ def test_admin_delete_discussion_creates_version_record(db):
assert v.old_title == "Admin will delete this"
assert v.new_title is None
assert v.editor_user_id == admin.id


def test_create_discussion_incomplete_profile(db):
user, token = generate_user(complete_profile=False)
user2, token2 = generate_user()

with session_scope() as session:
community_id = create_community(session, 0, 1, "Testing Community", [user2], [], None).id

with discussions_session(token) as api:
with pytest.raises(grpc.RpcError) as e:
api.CreateDiscussion(
discussions_pb2.CreateDiscussionReq(
title="dummy title",
content="dummy content",
owner_community_id=community_id,
)
)
assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
assert e.value.details() == "You have to complete your profile before you can start a discussion."
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ interface ProfileIncompleteDialogProps {
| "send_message"
| "send_request"
| "create_public_trip"
| "send_friend_request";
| "send_friend_request"
| "create_discussion";
}

export default function ProfileIncompleteDialog({
Expand Down Expand Up @@ -64,10 +65,18 @@ export default function ProfileIncompleteDialog({
</DialogContentText>
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={onClose}>
<Button
variant="outlined"
onClick={onClose}
sx={{ textAlign: "center" }}
>
{t("profile:complete_profile_dialog.cancel_button")}
</Button>
<Button component={Link} href={routeToEditProfile()}>
<Button
component={Link}
href={routeToEditProfile()}
sx={{ textAlign: "center" }}
>
{t("profile:complete_profile_dialog.edit_profile_button")}
</Button>
</DialogActions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import community from "test/fixtures/community.json";
import discussions from "test/fixtures/discussions.json";
import wrapper from "test/hookWrapper";
import i18n from "test/i18n";
import { getLiteUser } from "test/serviceMockDefaults";
import { getAccountInfo, getLiteUser } from "test/serviceMockDefaults";
import { assertErrorAlert, mockConsoleError, MockedService } from "test/utils";

import { DISCUSSION_CARD_TEST_ID } from "./DiscussionCard";
Expand All @@ -23,6 +23,9 @@ jest.mock("components/MarkdownInput");
const getLiteUserMock = service.user.getLiteUser as MockedService<
typeof service.user.getLiteUser
>;
const getAccountInfoMock = service.account.getAccountInfo as MockedService<
typeof service.account.getAccountInfo
>;
const createDiscussionMock = service.discussions
.createDiscussion as MockedService<
typeof service.discussions.createDiscussion
Expand All @@ -33,6 +36,7 @@ const listDiscussionsMock = service.communities
describe("DiscussionsListPage", () => {
beforeEach(() => {
getLiteUserMock.mockImplementation(getLiteUser);
getAccountInfoMock.mockImplementation(getAccountInfo);
listDiscussionsMock.mockResolvedValue({
discussionsList: discussions,
nextPageToken: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import Alert from "components/Alert";
import Button from "components/Button";
import CenteredSpinner from "components/CenteredSpinner/CenteredSpinner";
import { EmailIcon } from "components/Icons";
import ProfileIncompleteDialog from "components/ProfileIncompleteDialog/ProfileIncompleteDialog";
import TextBody from "components/TextBody";
import useAccountInfo from "features/auth/useAccountInfo";
import { SectionTitle } from "features/communities/CommunityPage";
import { useListDiscussions } from "features/communities/hooks";
import { useTranslation } from "i18n";
Expand Down Expand Up @@ -62,10 +64,13 @@ export default function DiscussionsListPage({
}) {
const { t } = useTranslation([COMMUNITIES]);

const { data: accountInfo } = useAccountInfo();
const hash = typeof window !== "undefined" ? window.location.hash : "";
// Intent to create a post, set by both the #new hash and the "New post" button.
const [isCreatingNewPost, setIsCreatingNewPost] = useState(
hash.includes("new"),
);

const {
isLoading: isDiscussionsLoading,
isFetching: isDiscussionsFetching,
Expand All @@ -78,8 +83,22 @@ export default function DiscussionsListPage({
// loading is false when refetched since there's old data in cache already
const isRefetching = !isDiscussionsLoading && isDiscussionsFetching;

// Derive form/dialog visibility during render so both entry points share the
// same gate: show the form only once the profile is known to be complete,
// otherwise show the dialog.
const profileIncomplete =
accountInfo !== undefined && !accountInfo.profileComplete;
const showCreateForm =
isCreatingNewPost && accountInfo?.profileComplete === true;
const profileDialogOpen = isCreatingNewPost && profileIncomplete;

return (
<>
<ProfileIncompleteDialog
open={profileDialogOpen}
onClose={() => setIsCreatingNewPost(false)}
attempted_action="create_discussion"
/>
<StyledDiscussionsHeader>
<SectionTitle icon={<EmailIcon />}>
{t("communities:discussions_title")}
Expand All @@ -88,7 +107,7 @@ export default function DiscussionsListPage({
{discussionsError && (
<Alert severity="error">{discussionsError.message}</Alert>
)}
<Collapse in={!isCreatingNewPost}>
<Collapse in={!showCreateForm}>
<StyledNewPostButtonContainer>
<StyledCreateResourceButton
onClick={() => setIsCreatingNewPost(true)}
Expand All @@ -98,7 +117,7 @@ export default function DiscussionsListPage({
{isRefetching && <CenteredSpinner />}
</StyledNewPostButtonContainer>
</Collapse>
<Collapse in={isCreatingNewPost}>
<Collapse in={showCreateForm}>
<CreateDiscussionForm
communityId={community.communityId}
onCancel={() => setIsCreatingNewPost(false)}
Expand Down
3 changes: 2 additions & 1 deletion app/web/features/profile/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,8 @@
"send_message": "send a message",
"send_friend_request": "send a friend request",
"create_event": "create an event",
"create_public_trip": "create a public trip"
"create_public_trip": "create a public trip",
"create_discussion": "create a discussion"
},
"edit_profile_button": "Edit your profile now",
"cancel_button": "Never mind"
Expand Down
Loading