Skip to content

Commit 988c2c9

Browse files
authored
Merge pull request #2797 from objectcomputing/feature-2786/anonymous-pulse-responses
Allow for anonymous pulse submission and viewing in the pulse report.
2 parents 7601fc3 + fa82c9d commit 988c2c9

File tree

11 files changed

+126
-64
lines changed

11 files changed

+126
-64
lines changed

server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponse.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public class PulseResponse {
5252

5353
@Column(name="teammemberid")
5454
@TypeDef(type=DataType.STRING)
55-
@NotNull
55+
@Nullable
5656
@Schema(description = "id of the teamMember this entry is associated with")
5757
private UUID teamMemberId;
5858

@@ -77,7 +77,7 @@ public class PulseResponse {
7777
protected PulseResponse() {
7878
}
7979

80-
public PulseResponse(UUID id, Integer internalScore, Integer externalScore, LocalDate submissionDate, UUID teamMemberId, String internalFeelings, String externalFeelings) {
80+
public PulseResponse(UUID id, Integer internalScore, Integer externalScore, LocalDate submissionDate, @Nullable UUID teamMemberId, String internalFeelings, String externalFeelings) {
8181
this.id = id;
8282
this.internalScore = internalScore;
8383
this.externalScore = externalScore;
@@ -88,7 +88,7 @@ public PulseResponse(UUID id, Integer internalScore, Integer externalScore, Loca
8888
}
8989

9090
public PulseResponse(Integer internalScore, Integer externalScore, LocalDate submissionDate, UUID teamMemberId, String internalFeelings, String externalFeelings) {
91-
this(null,internalScore, externalScore, submissionDate, teamMemberId, internalFeelings, externalFeelings);
91+
this(null, internalScore, externalScore, submissionDate, teamMemberId, internalFeelings, externalFeelings);
9292
}
9393

9494
public UUID getId() {

server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTO.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public class PulseResponseCreateDTO {
2727
@Schema(description = "date for submissionDate")
2828
private LocalDate submissionDate;
2929

30-
@NotNull
30+
@Nullable
3131
@Schema(description = "id of the associated member")
3232
private UUID teamMemberId;
3333

server/src/main/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseServicesImpl.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,14 @@ public PulseResponse save(PulseResponse pulseResponse) {
5555
LocalDate pulseSubDate = pulseResponse.getSubmissionDate();
5656
if (pulseResponse.getId() != null) {
5757
throw new BadArgException(String.format("Found unexpected id for pulseresponse %s", pulseResponse.getId()));
58-
} else if (memberRepo.findById(memberId).isEmpty()) {
58+
} else if (memberId != null &&
59+
memberRepo.findById(memberId).isEmpty()) {
5960
throw new BadArgException(String.format("Member %s doesn't exists", memberId));
6061
} else if (pulseSubDate.isBefore(LocalDate.EPOCH) || pulseSubDate.isAfter(LocalDate.MAX)) {
6162
throw new BadArgException(String.format("Invalid date for pulseresponse submission date %s", memberId));
62-
} else if (!currentUserId.equals(memberId) && !isSubordinateTo(memberId, currentUserId)) {
63+
} else if (memberId != null &&
64+
!currentUserId.equals(memberId) &&
65+
!isSubordinateTo(memberId, currentUserId)) {
6366
throw new BadArgException(String.format("User %s does not have permission to create pulse response for user %s", currentUserId, memberId));
6467
}
6568
pulseResponseRet = pulseResponseRepo.save(pulseResponse);
@@ -94,7 +97,7 @@ public PulseResponse update(PulseResponse pulseResponse) {
9497
} else if (memberRepo.findById(memberId).isEmpty()) {
9598
throw new BadArgException(String.format("Member %s doesn't exist", memberId));
9699
} else if (memberId == null) {
97-
throw new BadArgException(String.format("Invalid pulseresponse %s", pulseResponse));
100+
throw new BadArgException("Cannot update anonymous pulse response");
98101
} else if (pulseSubDate.isBefore(LocalDate.EPOCH) || pulseSubDate.isAfter(LocalDate.MAX)) {
99102
throw new BadArgException(String.format("Invalid date for pulseresponse submission date %s", memberId));
100103
} else if (!currentUserId.equals(memberId) && !isSubordinateTo(memberId, currentUserId)) {
@@ -191,4 +194,4 @@ public void sendPulseLowScoreEmail(PulseResponse pulseResponse) {
191194
emailSender.sendEmail(null, null, subject, bodyBuilder.toString(), recipients.toArray(new String[0]));
192195
}
193196
}
194-
}
197+
}

server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseControllerTest.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,12 @@ void testCreateAnInvalidPulseResponse() {
7676
JsonNode body = responseException.getResponse().getBody(JsonNode.class).orElse(null);
7777
JsonNode errors = Objects.requireNonNull(body).get("_embedded").get("errors");
7878
JsonNode href = Objects.requireNonNull(body).get("_links").get("self").get("href");
79-
List<String> errorList = Stream.of(errors.get(0).get("message").asText(), errors.get(1).get("message").asText(), errors.get(2).get("message").asText()).sorted().collect(Collectors.toList());
80-
assertEquals(3, errorList.size());
79+
List<String> errorList = Stream.of(
80+
errors.get(0).get("message").asText(),
81+
errors.get(1).get("message").asText()
82+
).sorted().collect(Collectors.toList());
83+
84+
assertEquals(2, errorList.size());
8185
assertEquals(request.getPath(), href.asText());
8286
assertEquals(HttpStatus.BAD_REQUEST, responseException.getStatus());
8387
}
@@ -523,4 +527,4 @@ private MemberProfile profile(String key) {
523527
private UUID id(String key) {
524528
return profile(key).getId();
525529
}
526-
}
530+
}

server/src/test/java/com/objectcomputing/checkins/services/pulseresponse/PulseResponseCreateDTOTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ void testConstraintViolation() {
3535
PulseResponseCreateDTO dto = new PulseResponseCreateDTO();
3636

3737
Set<ConstraintViolation<PulseResponseCreateDTO>> violations = validator.validate(dto);
38-
assertEquals(3, violations.size());
38+
assertEquals(2, violations.size());
3939
for (ConstraintViolation<PulseResponseCreateDTO> violation : violations) {
4040
assertEquals("must not be null", violation.getMessage());
4141
}

web-ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"fuse.js": "^6.4.6",
2929
"html-react-parser": "^5.1.12",
3030
"isomorphic-fetch": "^3.0.0",
31+
"js-cookie": "^3.0.5",
3132
"js-file-download": "^0.4.12",
3233
"lodash": "^4.17.21",
3334
"markdown-builder": "^0.9.0",

web-ui/src/pages/PulsePage.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,9 @@
1212
text-align: center;
1313
}
1414
}
15+
16+
.submit-row {
17+
align-items: center;
18+
display: flex;
19+
margin-top: 2rem; /* This is the default top margin for Buttons */
20+
}

web-ui/src/pages/PulsePage.jsx

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import Cookies from 'js-cookie';
12
import { format } from 'date-fns';
23
import React, { useContext, useEffect, useState } from 'react';
34
import { useHistory } from 'react-router-dom';
4-
import { Button, Typography } from '@mui/material';
5-
import { resolve } from '../api/api.js';
5+
import { Button, Checkbox, Typography } from '@mui/material';
6+
import { downloadData, initiate } from '../api/generic.js';
67
import Pulse from '../components/pulse/Pulse.jsx';
78
import { AppContext } from '../context/AppContext';
89
import { selectCsrfToken, selectCurrentUser } from '../context/selectors';
@@ -15,17 +16,26 @@ const PulsePage = () => {
1516
const { state } = useContext(AppContext);
1617
const currentUser = selectCurrentUser(state);
1718
const csrf = selectCsrfToken(state);
18-
const history = useHistory();
1919

2020
const [externalComment, setExternalComment] = useState('');
2121
const [externalScore, setExternalScore] = useState(center);
2222
const [internalComment, setInternalComment] = useState('');
2323
const [internalScore, setInternalScore] = useState(center);
2424
const [pulse, setPulse] = useState(null);
2525
const [submittedToday, setSubmittedToday] = useState(false);
26+
const [submitAnonymously, setSubmitAnonymously] = useState(false);
27+
2628
const today = format(new Date(), 'yyyy-MM-dd');
29+
const cookieName = "pulse_submitted_anonymously";
30+
const pulseURL = '/services/pulse-responses';
2731

2832
useEffect(() => {
33+
const submitted = Cookies.get(cookieName);
34+
if (submitted) {
35+
setSubmittedToday(true);
36+
return;
37+
}
38+
2939
if (!pulse) return;
3040

3141
const now = new Date();
@@ -52,19 +62,8 @@ const PulsePage = () => {
5262
dateTo: today,
5363
teamMemberId: currentUser.id
5464
};
55-
const queryString = Object.entries(query)
56-
.map(([key, value]) => `${key}=${value}`)
57-
.join('&');
58-
59-
const res = await resolve({
60-
method: 'GET',
61-
url: `/services/pulse-responses?${queryString}`,
62-
headers: {
63-
'X-CSRF-Header': csrf,
64-
Accept: 'application/json',
65-
'Content-Type': 'application/json;charset=UTF-8'
66-
}
67-
});
65+
66+
const res = await downloadData(pulseURL, csrf, query);
6867
if (res.error) return;
6968

7069
// Sort pulse responses by date, latest to earliest
@@ -97,22 +96,15 @@ const PulsePage = () => {
9796
internalScore: internalScore + 1, // converts to 1-based
9897
submissionDate: today,
9998
updatedDate: today,
100-
teamMemberId: myId
99+
teamMemberId: submitAnonymously ? null : myId,
101100
};
102-
const res = await resolve({
103-
method: 'POST',
104-
url: '/services/pulse-responses',
105-
headers: {
106-
'X-CSRF-Header': csrf,
107-
Accept: 'application/json',
108-
'Content-Type': 'application/json;charset=UTF-8'
109-
},
110-
data
111-
});
101+
const res = await initiate(pulseURL, csrf, data);
112102
if (res.error) return;
113103

114-
// Refresh browser to show that pulses where already submitted today.
115-
history.go(0);
104+
setSubmittedToday(true);
105+
if (submitAnonymously) {
106+
Cookies.set(cookieName, 'true', { expires: 1 });
107+
}
116108
};
117109

118110
return (
@@ -141,9 +133,25 @@ const PulsePage = () => {
141133
setScore={setExternalScore}
142134
title="How are you feeling about life outside of work?"
143135
/>
144-
<Button onClick={submit} variant="contained">
145-
Submit
146-
</Button>
136+
<div className="submit-row">
137+
<Button
138+
style={{ marginTop: 0 }}
139+
onClick={submit}
140+
variant="contained">
141+
Submit
142+
</Button>
143+
<div style={{ padding: '.3rem' }}/>
144+
<label>
145+
<Checkbox
146+
disableRipple
147+
id="submit-anonymously"
148+
type="checkbox"
149+
checked={submitAnonymously}
150+
onChange={(event) => setSubmitAnonymously(event.target.checked)}
151+
/>
152+
Submit Anonymously
153+
</label>
154+
</div>
147155
</>
148156
)}
149157
</div>

web-ui/src/pages/PulseReportPage.jsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ const PulseReportPage = () => {
161161

162162
for (const pulse of pulses) {
163163
const memberId = pulse.teamMemberId;
164-
if (!teamMemberIds.includes(memberId)) continue;
164+
if (memberId && !teamMemberIds.includes(memberId)) continue;
165165

166166
const { externalScore, internalScore, submissionDate } = pulse;
167167
const [year, month, day] = submissionDate;
@@ -181,18 +181,21 @@ const PulseReportPage = () => {
181181
frequencies[internalScore - 1].internal++;
182182
frequencies[externalScore - 1].external++;
183183

184-
const member = memberMap[memberId];
185-
const { supervisorid } = member;
186-
const memberIdToUse = managerMode ? supervisorid : memberId;
187-
188-
/* For debugging ...
189-
if (supervisorid) {
190-
const supervisor = memberMap[supervisorid];
191-
console.log(`The supervisor of ${member.name} is ${supervisor.name}`);
192-
} else {
193-
console.log(`${member.name} has no supervisor`);
184+
let memberIdToUse;
185+
if (memberId) {
186+
const member = memberMap[memberId];
187+
const { supervisorid } = member;
188+
memberIdToUse = managerMode ? supervisorid : memberId;
189+
190+
/* For debugging ...
191+
if (supervisorid) {
192+
const supervisor = memberMap[supervisorid];
193+
console.log(`The supervisor of ${member.name} is ${supervisor.name}`);
194+
} else {
195+
console.log(`${member.name} has no supervisor`);
196+
}
197+
*/
194198
}
195-
*/
196199

197200
// When in manager mode, if the member
198201
// doesn't have a supervisor then skip this data.

web-ui/src/pages/__snapshots__/PulsePage.test.jsx.snap

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -455,16 +455,48 @@ exports[`renders correctly 1`] = `
455455
</div>
456456
</div>
457457
</div>
458-
<button
459-
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButton-colorPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButton-colorPrimary css-sghohy-MuiButtonBase-root-MuiButton-root"
460-
tabindex="0"
461-
type="button"
458+
<div
459+
class="submit-row"
462460
>
463-
Submit
464-
<span
465-
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
461+
<button
462+
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButton-colorPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButton-colorPrimary css-sghohy-MuiButtonBase-root-MuiButton-root"
463+
style="margin-top: 0px;"
464+
tabindex="0"
465+
type="button"
466+
>
467+
Submit
468+
<span
469+
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
470+
/>
471+
</button>
472+
<div
473+
style="padding: .3rem;"
466474
/>
467-
</button>
475+
<label>
476+
<span
477+
class="MuiButtonBase-root MuiCheckbox-root MuiCheckbox-colorPrimary MuiCheckbox-sizeMedium PrivateSwitchBase-root MuiCheckbox-root MuiCheckbox-colorPrimary MuiCheckbox-sizeMedium MuiCheckbox-root MuiCheckbox-colorPrimary MuiCheckbox-sizeMedium css-355xbt-MuiButtonBase-root-MuiCheckbox-root"
478+
>
479+
<input
480+
class="PrivateSwitchBase-input css-1m9pwf3"
481+
data-indeterminate="false"
482+
id="submit-anonymously"
483+
type="checkbox"
484+
/>
485+
<svg
486+
aria-hidden="true"
487+
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root"
488+
data-testid="CheckBoxOutlineBlankIcon"
489+
focusable="false"
490+
viewBox="0 0 24 24"
491+
>
492+
<path
493+
d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"
494+
/>
495+
</svg>
496+
</span>
497+
Submit Anonymously
498+
</label>
499+
</div>
468500
</div>
469501
</div>
470502
`;

0 commit comments

Comments
 (0)