Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Major goal- and step-loading refactor. #1004

Merged
merged 11 commits into from
Feb 10, 2021
54 changes: 37 additions & 17 deletions Backend.Tests/Controllers/UserEditControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,25 +100,25 @@ public void TestCreateUserEdit()
}

[Test]
public void TestAddEditsToGoal()
public void TestAddGoalToUserEdit()
{
var userEdit = RandomUserEdit();
_userEditRepo.Create(userEdit);
var newEditStep = new Edit();
newEditStep.StepData.Add("This is a new step");
var updateEdit = userEdit.Clone();
updateEdit.Edits.Add(newEditStep);
var newEdit = new Edit();
newEdit.StepData.Add("This is a new step");
var updatedUserEdit = userEdit.Clone();
updatedUserEdit.Edits.Add(newEdit);

_ = _userEditController.Post(_projId, userEdit.Id, newEditStep).Result;
_ = _userEditController.Post(_projId, userEdit.Id, newEdit).Result;

var allUserEdits = _userEditRepo.GetAllUserEdits(_projId).Result;
Assert.Contains(updateEdit, allUserEdits);
Assert.Contains(updatedUserEdit, allUserEdits);
}

[Test]
public void TestGoalToUserEdit()
public void TestAddStepToGoal()
{
// Generate db entry to test
// Generate db entry to test.
var rnd = new Random();
var count = rnd.Next(1, 13);

Expand All @@ -128,17 +128,17 @@ public void TestGoalToUserEdit()
}
var origUserEdit = _userEditRepo.Create(RandomUserEdit()).Result;

// Generate correct result for comparison
// Generate correct result for comparison.
var modUserEdit = origUserEdit.Clone();
const string stringUserEdit = "This is another step added";
modUserEdit.Edits[0].StepData.Add(stringUserEdit);

// Create wrapper object
const string stringStep = "This is another step added.";
const int modGoalIndex = 0;
var wrapperObj = new UserEditObjectWrapper(modGoalIndex, stringUserEdit);
modUserEdit.Edits[modGoalIndex].StepData.Add(stringStep);

_ = _userEditController.Put(_projId, origUserEdit.Id, wrapperObj);
// Create and put wrapper object.
var stepWrapperObj = new UserEditStepWrapper(modGoalIndex, stringStep);
_ = _userEditController.Put(_projId, origUserEdit.Id, stepWrapperObj);

// Step count should have increased by 1.
Assert.That(_userEditRepo.GetAllUserEdits(_projId).Result, Has.Count.EqualTo(count + 1));

var userEdit = _userEditRepo.GetUserEdit(_projId, origUserEdit.Id).Result;
Expand All @@ -147,7 +147,27 @@ public void TestGoalToUserEdit()
Assert.Fail();
return;
}
Assert.Contains(stringUserEdit, userEdit.Edits[modGoalIndex].StepData);
Assert.Contains(stringStep, userEdit.Edits[modGoalIndex].StepData);

// Now update a step within the goal.
const string modStringStep = "This is a replacement step.";
const int modStepIndex = 1;
modUserEdit.Edits[modGoalIndex].StepData[modStepIndex] = modStringStep;

// Create and put wrapper object.
stepWrapperObj = new UserEditStepWrapper(modGoalIndex, modStringStep, modStepIndex);
_ = _userEditController.Put(_projId, origUserEdit.Id, stepWrapperObj);

// Step count should not have further increased.
Assert.That(_userEditRepo.GetAllUserEdits(_projId).Result, Has.Count.EqualTo(count + 1));

userEdit = _userEditRepo.GetUserEdit(_projId, origUserEdit.Id).Result;
if (userEdit is null)
{
Assert.Fail();
return;
}
Assert.Contains(modStringStep, userEdit.Edits[modGoalIndex].StepData);
}

[Test]
Expand Down
34 changes: 25 additions & 9 deletions Backend/Controllers/UserEditController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,12 @@ public async Task<IActionResult> Post(string projectId, string userEditId, [From
return new NotFoundObjectResult(editIndex);
}

/// <summary> Adds a step to specified goal </summary>
/// <summary> Adds/updates a step to/in specified goal </summary>
/// <remarks> PUT: v1/projects/{projectId}/useredits/{userEditId} </remarks>
/// <returns> Index of newest edit </returns>
/// <returns> Index of added/modified step in specified goal </returns>
[HttpPut("{userEditId}")]
public async Task<IActionResult> Put(string projectId, string userEditId,
[FromBody] UserEditObjectWrapper userEdit)
[FromBody] UserEditStepWrapper stepEdit)
{
if (!await _permissionService.HasProjectPermission(HttpContext, Permission.WordEntry))
{
Expand All @@ -208,22 +208,38 @@ public async Task<IActionResult> Put(string projectId, string userEditId,
return new NotFoundObjectResult(projectId);
}

// Ensure userEdit exists
// Ensure userEdit exists.
var document = await _repo.GetUserEdit(projectId, userEditId);
if (document is null)
{
return new NotFoundResult();
}

// Ensure index exists
if (userEdit.GoalIndex >= document.Edits.Count)
// Ensure indices exist.
if (stepEdit.GoalIndex < 0 || stepEdit.GoalIndex >= document.Edits.Count)
{
return new BadRequestObjectResult("Goal index out of range.");
}
var maxStepIndex = document.Edits[stepEdit.GoalIndex].StepData.Count;
var stepIndex = stepEdit.StepIndex ?? maxStepIndex;
if (stepIndex < 0 || stepIndex > maxStepIndex)
{
return new BadRequestObjectResult("Goal index out of range");
return new BadRequestObjectResult("Step index out of range.");
}

await _userEditService.AddStepToGoal(projectId, userEditId, userEdit.GoalIndex, userEdit.NewEdit);
// Add new step to or update step in goal.
if (stepIndex == maxStepIndex)
{
await _userEditService.AddStepToGoal(
projectId, userEditId, stepEdit.GoalIndex, stepEdit.StepString);
}
else
{
await _userEditService.UpdateStepInGoal(
projectId, userEditId, stepEdit.GoalIndex, stepEdit.StepString, stepIndex);
}

return new OkObjectResult(document.Edits[userEdit.GoalIndex].StepData.Count - 1);
return new OkObjectResult(stepIndex);
}

/// <summary> Deletes <see cref="UserEdit"/> with specified id </summary>
Expand Down
3 changes: 2 additions & 1 deletion Backend/Interfaces/IUserEditService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ namespace BackendFramework.Interfaces
{
public interface IUserEditService
{
Task<bool> AddStepToGoal(string projectId, string userEditId, int goalIndex, string userEdit);
Task<Tuple<bool, int>> AddGoalToUserEdit(string projectId, string userEditId, Edit edit);
Task<bool> AddStepToGoal(string projectId, string userEditId, int goalIndex, string stepString);
Task<bool> UpdateStepInGoal(string projectId, string userEditId, int goalIndex, string stepString, int stepIndex);
}
}
23 changes: 15 additions & 8 deletions Backend/Models/UserEdit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,33 +67,40 @@ public override int GetHashCode()
}
}

public class UserEditObjectWrapper
public class UserEditStepWrapper
{
[BsonElement("goalIndex")]
public int GoalIndex { get; set; }

[BsonElement("newEdit")]
public string NewEdit { get; set; }
[BsonElement("stepString")]
public string StepString { get; set; }

public UserEditObjectWrapper(int goalIndex, string newEdit)
[BsonElement("stepIndex")]
/* A null StepIndex implies index equal to the length of the step list--
* i.e. the step is to be added to the end of the list. */
public int? StepIndex { get; set; }

public UserEditStepWrapper(int goalIndex, string stepString, int? stepIndex = null)
{
GoalIndex = goalIndex;
NewEdit = newEdit;
StepString = stepString;
StepIndex = stepIndex;
}

public override bool Equals(object? obj)
{
if (!(obj is UserEditObjectWrapper other) || GetType() != obj.GetType())
if (!(obj is UserEditStepWrapper other) || GetType() != obj.GetType())
{
return false;
}

return other.GoalIndex.Equals(GoalIndex) && other.NewEdit.Equals(NewEdit);
return other.GoalIndex.Equals(GoalIndex)
&& other.StepString.Equals(StepString) && other.StepIndex.Equals(StepIndex);
}

public override int GetHashCode()
{
return HashCode.Combine(GoalIndex, NewEdit);
return HashCode.Combine(GoalIndex, StepString, StepIndex);
}
}

Expand Down
36 changes: 27 additions & 9 deletions Backend/Services/UserEditApiServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ public UserEditService(IUserEditRepository repo)
public async Task<Tuple<bool, int>> AddGoalToUserEdit(string projectId, string userEditId, Edit edit)
{
// Get userEdit to change
var userEntry = await _repo.GetUserEdit(projectId, userEditId);
var oldUserEdit = await _repo.GetUserEdit(projectId, userEditId);
const int invalidEditIndex = -1;
var failureResult = new Tuple<bool, int>(false, invalidEditIndex);
if (userEntry is null)
if (oldUserEdit is null)
{
return failureResult;
}

var newUserEdit = userEntry.Clone();
var newUserEdit = oldUserEdit.Clone();

// Add the new goal index to Edits list
newUserEdit.Edits.Add(edit);
Expand All @@ -54,18 +54,36 @@ public async Task<Tuple<bool, int>> AddGoalToUserEdit(string projectId, string u
return new Tuple<bool, int>(replaceSucceeded, indexOfNewestEdit);
}

/// <summary> Adds a string representation of a step to a <see cref="Edit"/> at a specified index </summary>
/// <summary> Adds a string representation of a step to a specified <see cref="Edit"/> </summary>
/// <returns> A bool: success of operation </returns>
public async Task<bool> AddStepToGoal(string projectId, string userEditId, int goalIndex, string userEdit)
public async Task<bool> AddStepToGoal(string projectId, string userEditId, int goalIndex, string newStep)
{
var addUserEdit = await _repo.GetUserEdit(projectId, userEditId);
if (addUserEdit is null)
var oldUserEdit = await _repo.GetUserEdit(projectId, userEditId);
if (oldUserEdit is null || goalIndex >= oldUserEdit.Edits.Count)
{
return false;
}

var newUserEdit = addUserEdit.Clone();
newUserEdit.Edits[goalIndex].StepData.Add(userEdit);
var newUserEdit = oldUserEdit.Clone();
newUserEdit.Edits[goalIndex].StepData.Add(newStep);
var updateResult = await _repo.Replace(projectId, userEditId, newUserEdit);
return updateResult;
}

/// <summary> Updates a specified step to in a specified <see cref="Edit"/> </summary>
/// <returns> A bool: success of operation </returns>
public async Task<bool> UpdateStepInGoal(
string projectId, string userEditId, int goalIndex, string updatedStep, int stepIndex)
{
var oldUserEdit = await _repo.GetUserEdit(projectId, userEditId);
if (oldUserEdit is null || goalIndex >= oldUserEdit.Edits.Count
|| stepIndex >= oldUserEdit.Edits[goalIndex].StepData.Count)
{
return false;
}

var newUserEdit = oldUserEdit.Clone();
newUserEdit.Edits[goalIndex].StepData[stepIndex] = updatedStep;
var updateResult = await _repo.Replace(projectId, userEditId, newUserEdit);
return updateResult;
}
Expand Down
31 changes: 19 additions & 12 deletions src/backend/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import axios from "axios";

import authHeader from "components/Login/AuthHeaders";
import history, { Path } from "browserHistory";
import { Goal } from "types/goals";
import { Goal, GoalStep } from "types/goals";
import { Project } from "types/project";
import { RuntimeConfig } from "types/runtimeConfig";
import SemanticDomainWithSubdomains from "types/SemanticDomain";
Expand Down Expand Up @@ -400,15 +400,20 @@ export async function avatarSrc(userId: string): Promise<string> {
return `data:${resp.headers["content-type"].toLowerCase()};base64,${image}`;
}

function convertGoalToBackendEdit(goal: Goal) {
const stepData = goal.steps.map((s) => JSON.stringify(s));
return {
goalType: goal.goalType.toString(),
stepData,
};
}

/** Returns index of added goal */
export async function addGoalToUserEdit(
userEditId: string,
goal: Goal
): Promise<Goal> {
const stepData = JSON.stringify(goal.steps);
const userEditTuple = {
goalType: goal.goalType.toString(),
stepData: [stepData],
};
): Promise<number> {
const userEditTuple = convertGoalToBackendEdit(goal);
const projectId = LocalStorage.getProjectId();
const resp = await backendServer.post(
`projects/${projectId}/useredits/${userEditId}`,
Expand All @@ -420,17 +425,19 @@ export async function addGoalToUserEdit(
return resp.data;
}

/** Returns index of step within specified goal */
export async function addStepToGoal(
userEditId: string,
goalIndex: number,
goal: Goal
): Promise<Goal> {
const newEdit = JSON.stringify(goal.steps);
const userEditTuple = { goalIndex, newEdit };
step: GoalStep,
stepIndex?: number // If undefined, step will be added to end.
): Promise<number> {
const stepString = JSON.stringify(step);
const stepEditTuple = { goalIndex, stepString, stepIndex };
return await backendServer
.put(
`projects/${LocalStorage.getProjectId()}/useredits/${userEditId}`,
userEditTuple,
stepEditTuple,
{
headers: { ...authHeader() },
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from "react";
import { Provider } from "react-redux";
import renderer, {
ReactTestInstance,
Expand Down
7 changes: 4 additions & 3 deletions src/components/GoalTimeline/GoalDisplay/HorizontalDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";
import { Translate } from "react-localize-redux";
import { Typography, GridList, GridListTile, Button } from "@material-ui/core";

import { Goal } from "types/goals";
import { Goal, GoalType } from "types/goals";

const style = {
container: {
Expand All @@ -23,7 +23,7 @@ interface HorizontalDisplayProps {
width: number;
numPanes: number;
scrollToEnd: boolean;
handleChange: (name: string) => void;
handleChange: (goalType: GoalType) => void;
}

export default class HorizontalDisplay extends React.Component<HorizontalDisplayProps> {
Expand All @@ -47,8 +47,9 @@ export default class HorizontalDisplay extends React.Component<HorizontalDisplay
{ ...style.buttonStyle, width: this.optionWidth + "vw" } as any
}
onClick={() => {
this.props.handleChange(goal.name);
this.props.handleChange(goal.goalType);
}}
disabled={goal.completed}
>
<Typography variant={"h6"}>
<Translate id={goal.name + ".title"} />
Expand Down
7 changes: 4 additions & 3 deletions src/components/GoalTimeline/GoalDisplay/VerticalDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Typography, GridList, GridListTile, Button } from "@material-ui/core";
import React from "react";
import { Translate } from "react-localize-redux";

import { Goal } from "types/goals";
import { Goal, GoalType } from "types/goals";

const style = {
container: {
Expand All @@ -23,7 +23,7 @@ interface VerticalDisplayProps {
height: number;
numPanes: number;
scrollToEnd: boolean;
handleChange: (name: string) => void;
handleChange: (goalType: GoalType) => void;
}

interface VerticalDisplayStates {
Expand Down Expand Up @@ -54,9 +54,10 @@ export default class VerticalDisplay extends React.Component<
{ ...style.buttonStyle, height: this.optionHeight + "vw" } as any
}
onClick={() => {
this.props.handleChange(goal.name);
this.props.handleChange(goal.goalType);
}}
fullWidth
disabled={goal.completed}
>
<Typography variant={"h6"}>
<Translate id={goal.name + ".title"} />
Expand Down
Loading