diff --git a/adhocracy4/follows/static/follows/FollowButton.jsx b/adhocracy4/follows/static/follows/FollowButton.jsx index 5d4737323..5a6abaa1c 100644 --- a/adhocracy4/follows/static/follows/FollowButton.jsx +++ b/adhocracy4/follows/static/follows/FollowButton.jsx @@ -2,11 +2,16 @@ import django from 'django' import React, { useState, useEffect } from 'react' import Alert from '../../../static/Alert' -const api = require('../../../static/api') +import api from '../../../static/api' +import config from '../../../static/config' const translated = { - followDescription: django.gettext('Click to be updated about this project via email.'), - followingDescription: django.gettext('Click to no longer be updated about this project via email.'), + followDescription: django.gettext( + 'Click to be updated about this project via email.' + ), + followingDescription: django.gettext( + 'Click to no longer be updated about this project via email.' + ), followAlert: django.gettext('You will be updated via email.'), followingAlert: django.gettext('You will no longer be updated via email.'), follow: django.gettext('Follow'), @@ -17,9 +22,7 @@ export const FollowButton = (props) => { const [following, setFollowing] = useState(null) const [alert, setAlert] = useState(null) - const followBtnText = following - ? translated.following - : translated.follow + const followBtnText = following ? translated.following : translated.follow const followDescriptionText = following ? translated.followingDescription @@ -30,44 +33,54 @@ export const FollowButton = (props) => { : translated.followAlert useEffect(() => { - api.follow.get(props.project) - .done((follow) => { - setFollowing(follow.enabled) - setAlert(follow.alert) - }) - .fail((response) => { - if (response.status === 404) { - setFollowing(false) - } - }) - }, [props.project]) + if (props.authenticatedAs) { + api.follow + .get(props.project) + .done((follow) => { + setFollowing(follow.enabled) + setAlert(follow.alert) + }) + .fail((response) => { + if (response.status === 404) { + setFollowing(false) + } + }) + } + }, [props.project, props.authenticatedAs]) const removeAlert = () => { setAlert(null) } const toggleFollow = () => { - api.follow.change({ enabled: !following }, props.project) - .done((follow) => { - setFollowing(follow.enabled) - setAlert({ - type: 'success', - message: followAlertText - }) + if (props.authenticatedAs === null) { + window.location.href = config.getLoginUrl() + return + } + api.follow.change({ enabled: !following }, props.project).done((follow) => { + setFollowing(follow.enabled) + setAlert({ + type: 'success', + message: followAlertText }) + }) } return ( diff --git a/adhocracy4/follows/static/follows/__tests__/FollowButton.jest.jsx b/adhocracy4/follows/static/follows/__tests__/FollowButton.jest.jsx new file mode 100644 index 000000000..f4d3eb20f --- /dev/null +++ b/adhocracy4/follows/static/follows/__tests__/FollowButton.jest.jsx @@ -0,0 +1,68 @@ +import React from 'react' +import { render, fireEvent, screen } from '@testing-library/react' +import '@testing-library/jest-dom' +import { FollowButton } from '../FollowButton' +import api from '../../../../static/api' + +// mock api and config, as they rely on network and browser +jest.mock('../../../../static/config') +jest.mock('../../../../static/api') + +afterEach(() => { + jest.clearAllMocks() +}) + +test('Test render FollowButton not following', async () => { + api.follow.setFollowing({ enabled: false }) + render() + const followButton = await screen.findByText('Follow') + expect(followButton).toBeTruthy() + const followingButton = screen.queryByText('Following') + expect(followingButton).toBeNull() + expect(api.follow.get).toHaveBeenCalledTimes(1) +}) + +test('Test render FollowButton following', async () => { + api.follow.setFollowing({ enabled: true }) + render() + const followingButton = await screen.findByText('Following') + expect(followingButton).toBeTruthy() + const followButton = screen.queryByText('Follow') + expect(followButton).toBeNull() + expect(api.follow.get).toHaveBeenCalledTimes(1) +}) + +test('Test render FollowButton click follow', async () => { + api.follow.setFollowing({ enabled: false }) + render() + let followButton = await screen.findByText('Follow') + expect(followButton).toBeTruthy() + let followingButton = screen.queryByText('Following') + expect(followingButton).toBeNull() + fireEvent.click(followButton) + followingButton = await screen.findByText('Following') + expect(followingButton).toBeTruthy() + followButton = screen.queryByText('Follow') + expect(followButton).toBeNull() + expect(api.follow.change).toHaveBeenCalledTimes(1) + expect(api.follow.get).toHaveBeenCalledTimes(1) +}) + +test('Test FollowButton redirect', async () => { + // testing the redirect doesn't work and will throw an exception + // as we are not in a browser. + // workaround: delete location and simply check if href is set + // to "correct" url + delete window.location + window.location = {} + api.follow.setFollowing({ enabled: false }) + render() + const followButton = await screen.findByText('Follow') + expect(followButton).toBeTruthy() + const followingButton = screen.queryByText('Following') + expect(followingButton).toBeNull() + fireEvent.click(followButton) + expect(window.location.href).toBe('/mock-url') + expect(api.follow.change).not.toHaveBeenCalled() + expect(api.follow.get).not.toHaveBeenCalled() +}) diff --git a/adhocracy4/follows/static/follows/react_follows.jsx b/adhocracy4/follows/static/follows/react_follows.jsx index e78d2b55e..0fb9492e6 100644 --- a/adhocracy4/follows/static/follows/react_follows.jsx +++ b/adhocracy4/follows/static/follows/react_follows.jsx @@ -4,7 +4,7 @@ import { createRoot } from 'react-dom/client' import { FollowButton } from './FollowButton' module.exports.renderFollow = function (el) { - const project = el.getAttribute('data-project') + const props = JSON.parse(el.getAttribute('data-attributes')) const root = createRoot(el) - root.render() + root.render() } diff --git a/adhocracy4/follows/templatetags/react_follows.py b/adhocracy4/follows/templatetags/react_follows.py index 7ae1280e2..06d20cbf4 100644 --- a/adhocracy4/follows/templatetags/react_follows.py +++ b/adhocracy4/follows/templatetags/react_follows.py @@ -1,12 +1,20 @@ +import json + from django import template from django.utils.html import format_html register = template.Library() -@register.simple_tag() -def react_follows(project): +@register.simple_tag(takes_context=True) +def react_follows(context, project): + request = context["request"] + user = request.user + authenticated_as = None + if user.is_authenticated: + authenticated_as = user.username + attributes = {"project": project.name, "authenticatedAs": authenticated_as} return format_html( - '', - project=project.slug, + '', + attributes=json.dumps(attributes), ) diff --git a/adhocracy4/static/__mocks__/api.js b/adhocracy4/static/__mocks__/api.js new file mode 100644 index 000000000..dd4f77ab7 --- /dev/null +++ b/adhocracy4/static/__mocks__/api.js @@ -0,0 +1,46 @@ +let following = null + +const api = { + follow: { + get: jest.fn(() => { + const instance = { + done: (fn) => { + if (following !== null) { + fn(following) + } + return instance + }, + fail: (fn) => { + if (following === null) { + fn({ status: 400 }) + } + return instance + } + } + return instance + }), + change: jest.fn((enabled) => { + following = { enabled } + const instance = { + done: (fn) => { + if (following !== null) { + fn(following) + } + return instance + }, + fail: (fn) => { + if (following === null) { + fn({ status: 400 }) + } + return instance + } + } + return instance + }), + setFollowing: (value) => { + following = value + } + } +} + +module.exports = api diff --git a/adhocracy4/static/__mocks__/config.js b/adhocracy4/static/__mocks__/config.js new file mode 100644 index 000000000..7252d4010 --- /dev/null +++ b/adhocracy4/static/__mocks__/config.js @@ -0,0 +1,3 @@ +module.exports = { + getLoginUrl: () => '/mock-url' +} diff --git a/changelog/7618.md b/changelog/7618.md index f385f99ae..38525164a 100644 --- a/changelog/7618.md +++ b/changelog/7618.md @@ -1,3 +1,5 @@ ### Changed - refactor follow to be functional and add aria described by for when no alert shown and use a4 prefix for classes so external style liberies can be used (story !7618/7701) +- redirect to login page when follow button is pressed and user is not logged + in