Skip to content

Commit

Permalink
Merge pull request #2797 from objectcomputing/feature-2786/anonymous-…
Browse files Browse the repository at this point in the history
…pulse-responses

Allow for anonymous pulse submission and viewing in the pulse report.
  • Loading branch information
mkimberlin authored Dec 23, 2024
2 parents 7601fc3 + fa82c9d commit 988c2c9
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public class PulseResponse {

@Column(name="teammemberid")
@TypeDef(type=DataType.STRING)
@NotNull
@Nullable
@Schema(description = "id of the teamMember this entry is associated with")
private UUID teamMemberId;

Expand All @@ -77,7 +77,7 @@ public class PulseResponse {
protected PulseResponse() {
}

public PulseResponse(UUID id, Integer internalScore, Integer externalScore, LocalDate submissionDate, UUID teamMemberId, String internalFeelings, String externalFeelings) {
public PulseResponse(UUID id, Integer internalScore, Integer externalScore, LocalDate submissionDate, @Nullable UUID teamMemberId, String internalFeelings, String externalFeelings) {
this.id = id;
this.internalScore = internalScore;
this.externalScore = externalScore;
Expand All @@ -88,7 +88,7 @@ public PulseResponse(UUID id, Integer internalScore, Integer externalScore, Loca
}

public PulseResponse(Integer internalScore, Integer externalScore, LocalDate submissionDate, UUID teamMemberId, String internalFeelings, String externalFeelings) {
this(null,internalScore, externalScore, submissionDate, teamMemberId, internalFeelings, externalFeelings);
this(null, internalScore, externalScore, submissionDate, teamMemberId, internalFeelings, externalFeelings);
}

public UUID getId() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class PulseResponseCreateDTO {
@Schema(description = "date for submissionDate")
private LocalDate submissionDate;

@NotNull
@Nullable
@Schema(description = "id of the associated member")
private UUID teamMemberId;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,14 @@ public PulseResponse save(PulseResponse pulseResponse) {
LocalDate pulseSubDate = pulseResponse.getSubmissionDate();
if (pulseResponse.getId() != null) {
throw new BadArgException(String.format("Found unexpected id for pulseresponse %s", pulseResponse.getId()));
} else if (memberRepo.findById(memberId).isEmpty()) {
} else if (memberId != null &&
memberRepo.findById(memberId).isEmpty()) {
throw new BadArgException(String.format("Member %s doesn't exists", memberId));
} else if (pulseSubDate.isBefore(LocalDate.EPOCH) || pulseSubDate.isAfter(LocalDate.MAX)) {
throw new BadArgException(String.format("Invalid date for pulseresponse submission date %s", memberId));
} else if (!currentUserId.equals(memberId) && !isSubordinateTo(memberId, currentUserId)) {
} else if (memberId != null &&
!currentUserId.equals(memberId) &&
!isSubordinateTo(memberId, currentUserId)) {
throw new BadArgException(String.format("User %s does not have permission to create pulse response for user %s", currentUserId, memberId));
}
pulseResponseRet = pulseResponseRepo.save(pulseResponse);
Expand Down Expand Up @@ -94,7 +97,7 @@ public PulseResponse update(PulseResponse pulseResponse) {
} else if (memberRepo.findById(memberId).isEmpty()) {
throw new BadArgException(String.format("Member %s doesn't exist", memberId));
} else if (memberId == null) {
throw new BadArgException(String.format("Invalid pulseresponse %s", pulseResponse));
throw new BadArgException("Cannot update anonymous pulse response");
} else if (pulseSubDate.isBefore(LocalDate.EPOCH) || pulseSubDate.isAfter(LocalDate.MAX)) {
throw new BadArgException(String.format("Invalid date for pulseresponse submission date %s", memberId));
} else if (!currentUserId.equals(memberId) && !isSubordinateTo(memberId, currentUserId)) {
Expand Down Expand Up @@ -191,4 +194,4 @@ public void sendPulseLowScoreEmail(PulseResponse pulseResponse) {
emailSender.sendEmail(null, null, subject, bodyBuilder.toString(), recipients.toArray(new String[0]));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,12 @@ void testCreateAnInvalidPulseResponse() {
JsonNode body = responseException.getResponse().getBody(JsonNode.class).orElse(null);
JsonNode errors = Objects.requireNonNull(body).get("_embedded").get("errors");
JsonNode href = Objects.requireNonNull(body).get("_links").get("self").get("href");
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());
assertEquals(3, errorList.size());
List<String> errorList = Stream.of(
errors.get(0).get("message").asText(),
errors.get(1).get("message").asText()
).sorted().collect(Collectors.toList());

assertEquals(2, errorList.size());
assertEquals(request.getPath(), href.asText());
assertEquals(HttpStatus.BAD_REQUEST, responseException.getStatus());
}
Expand Down Expand Up @@ -523,4 +527,4 @@ private MemberProfile profile(String key) {
private UUID id(String key) {
return profile(key).getId();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ void testConstraintViolation() {
PulseResponseCreateDTO dto = new PulseResponseCreateDTO();

Set<ConstraintViolation<PulseResponseCreateDTO>> violations = validator.validate(dto);
assertEquals(3, violations.size());
assertEquals(2, violations.size());
for (ConstraintViolation<PulseResponseCreateDTO> violation : violations) {
assertEquals("must not be null", violation.getMessage());
}
Expand Down
1 change: 1 addition & 0 deletions web-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"fuse.js": "^6.4.6",
"html-react-parser": "^5.1.12",
"isomorphic-fetch": "^3.0.0",
"js-cookie": "^3.0.5",
"js-file-download": "^0.4.12",
"lodash": "^4.17.21",
"markdown-builder": "^0.9.0",
Expand Down
6 changes: 6 additions & 0 deletions web-ui/src/pages/PulsePage.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@
text-align: center;
}
}

.submit-row {
align-items: center;
display: flex;
margin-top: 2rem; /* This is the default top margin for Buttons */
}
72 changes: 40 additions & 32 deletions web-ui/src/pages/PulsePage.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Cookies from 'js-cookie';
import { format } from 'date-fns';
import React, { useContext, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Button, Typography } from '@mui/material';
import { resolve } from '../api/api.js';
import { Button, Checkbox, Typography } from '@mui/material';
import { downloadData, initiate } from '../api/generic.js';
import Pulse from '../components/pulse/Pulse.jsx';
import { AppContext } from '../context/AppContext';
import { selectCsrfToken, selectCurrentUser } from '../context/selectors';
Expand All @@ -15,17 +16,26 @@ const PulsePage = () => {
const { state } = useContext(AppContext);
const currentUser = selectCurrentUser(state);
const csrf = selectCsrfToken(state);
const history = useHistory();

const [externalComment, setExternalComment] = useState('');
const [externalScore, setExternalScore] = useState(center);
const [internalComment, setInternalComment] = useState('');
const [internalScore, setInternalScore] = useState(center);
const [pulse, setPulse] = useState(null);
const [submittedToday, setSubmittedToday] = useState(false);
const [submitAnonymously, setSubmitAnonymously] = useState(false);

const today = format(new Date(), 'yyyy-MM-dd');
const cookieName = "pulse_submitted_anonymously";
const pulseURL = '/services/pulse-responses';

useEffect(() => {
const submitted = Cookies.get(cookieName);
if (submitted) {
setSubmittedToday(true);
return;
}

if (!pulse) return;

const now = new Date();
Expand All @@ -52,19 +62,8 @@ const PulsePage = () => {
dateTo: today,
teamMemberId: currentUser.id
};
const queryString = Object.entries(query)
.map(([key, value]) => `${key}=${value}`)
.join('&');

const res = await resolve({
method: 'GET',
url: `/services/pulse-responses?${queryString}`,
headers: {
'X-CSRF-Header': csrf,
Accept: 'application/json',
'Content-Type': 'application/json;charset=UTF-8'
}
});

const res = await downloadData(pulseURL, csrf, query);
if (res.error) return;

// Sort pulse responses by date, latest to earliest
Expand Down Expand Up @@ -97,22 +96,15 @@ const PulsePage = () => {
internalScore: internalScore + 1, // converts to 1-based
submissionDate: today,
updatedDate: today,
teamMemberId: myId
teamMemberId: submitAnonymously ? null : myId,
};
const res = await resolve({
method: 'POST',
url: '/services/pulse-responses',
headers: {
'X-CSRF-Header': csrf,
Accept: 'application/json',
'Content-Type': 'application/json;charset=UTF-8'
},
data
});
const res = await initiate(pulseURL, csrf, data);
if (res.error) return;

// Refresh browser to show that pulses where already submitted today.
history.go(0);
setSubmittedToday(true);
if (submitAnonymously) {
Cookies.set(cookieName, 'true', { expires: 1 });
}
};

return (
Expand Down Expand Up @@ -141,9 +133,25 @@ const PulsePage = () => {
setScore={setExternalScore}
title="How are you feeling about life outside of work?"
/>
<Button onClick={submit} variant="contained">
Submit
</Button>
<div className="submit-row">
<Button
style={{ marginTop: 0 }}
onClick={submit}
variant="contained">
Submit
</Button>
<div style={{ padding: '.3rem' }}/>
<label>
<Checkbox
disableRipple
id="submit-anonymously"
type="checkbox"
checked={submitAnonymously}
onChange={(event) => setSubmitAnonymously(event.target.checked)}
/>
Submit Anonymously
</label>
</div>
</>
)}
</div>
Expand Down
27 changes: 15 additions & 12 deletions web-ui/src/pages/PulseReportPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ const PulseReportPage = () => {

for (const pulse of pulses) {
const memberId = pulse.teamMemberId;
if (!teamMemberIds.includes(memberId)) continue;
if (memberId && !teamMemberIds.includes(memberId)) continue;

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

const member = memberMap[memberId];
const { supervisorid } = member;
const memberIdToUse = managerMode ? supervisorid : memberId;

/* For debugging ...
if (supervisorid) {
const supervisor = memberMap[supervisorid];
console.log(`The supervisor of ${member.name} is ${supervisor.name}`);
} else {
console.log(`${member.name} has no supervisor`);
let memberIdToUse;
if (memberId) {
const member = memberMap[memberId];
const { supervisorid } = member;
memberIdToUse = managerMode ? supervisorid : memberId;

/* For debugging ...
if (supervisorid) {
const supervisor = memberMap[supervisorid];
console.log(`The supervisor of ${member.name} is ${supervisor.name}`);
} else {
console.log(`${member.name} has no supervisor`);
}
*/
}
*/

// When in manager mode, if the member
// doesn't have a supervisor then skip this data.
Expand Down
48 changes: 40 additions & 8 deletions web-ui/src/pages/__snapshots__/PulsePage.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -455,16 +455,48 @@ exports[`renders correctly 1`] = `
</div>
</div>
</div>
<button
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"
tabindex="0"
type="button"
<div
class="submit-row"
>
Submit
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
<button
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"
style="margin-top: 0px;"
tabindex="0"
type="button"
>
Submit
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
<div
style="padding: .3rem;"
/>
</button>
<label>
<span
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"
>
<input
class="PrivateSwitchBase-input css-1m9pwf3"
data-indeterminate="false"
id="submit-anonymously"
type="checkbox"
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root"
data-testid="CheckBoxOutlineBlankIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
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"
/>
</svg>
</span>
Submit Anonymously
</label>
</div>
</div>
</div>
`;
5 changes: 5 additions & 0 deletions web-ui/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4627,6 +4627,11 @@ jest-fetch-mock@^3.0.3:
cross-fetch "^3.0.4"
promise-polyfill "^8.1.3"

js-cookie@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc"
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==

js-file-download@^0.4.12:
version "0.4.12"
resolved "https://registry.yarnpkg.com/js-file-download/-/js-file-download-0.4.12.tgz#10c70ef362559a5b23cdbdc3bd6f399c3d91d821"
Expand Down

0 comments on commit 988c2c9

Please sign in to comment.