<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.expertiza.ncsu.edu/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Spmclell</id>
	<title>Expertiza_Wiki - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.expertiza.ncsu.edu/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Spmclell"/>
	<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Special:Contributions/Spmclell"/>
	<updated>2026-05-06T18:18:22Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.41.0</generator>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024&amp;diff=160837</id>
		<title>CSC/ECE 517 Fall 2024</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024&amp;diff=160837"/>
		<updated>2024-12-05T18:46:16Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;* [[CSC/ECE 517 Fall 2024 - E2450. Refactor assignments_controller.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2451. Reimplement feedback_response_map.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2452. Refactor review_mapping_controller.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2453. Refactor review_mapping_helper.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2454. Refactor student_task.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2455. Refactor sign_up_sheet_controller.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2456. Refactor teams_user.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2456. Refactor teams_user.rb (Phase 2 - Design Document)]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2457. GitHub metrics integration]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2458. User management and users table]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2459. View for results of bidding]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2460 Mentor-Meeting Management]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2461. UI for Courses]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2461. UI and Backend for Courses]]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2462._UI_for_Questionnaires CSC/ECE 517 Fall 2024 - E2462. UI for Questionnaire.rb]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2463_Implement_Front_End_for_Student_Task_List CSC/ECE 517 Fall 2024 - E2463. UI for Student Task List]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2464 UI for Project Topics (was: Sign_up_Topics)]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2465. UI for Institutions and Notification]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2466. UI for Impersonate User]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2467. UI for View Submissions]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2468. Reimplement due_date]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2469. Reimplement grades/view_team]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2470. Reimplement grades_controller]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2471. Reimplement logger]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2472. Reimplement responses_controller.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2473. Reimplement sign up topic.rb as project topic.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2474. Reimplement student_quizzes_controller.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2475. Reimplement student_task view]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2476. Reimplement student_teams_controller.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion_controller.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion_controller.rb (Design Document)]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2478. Reimplement the Question hierarchy as Item hierarchy]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2479. Reimplement teams_users_controller.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2480. Implement testing for new Bookmarks Controller]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2481 Reimplement response_map.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2482. Reimplement heatgrid for reviews]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2483. Reimplement Notification Controller and Model]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2484. Reimplement participants_controller.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2485. Allow reviewers to bid on what to review]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2486.Reimplement deadline_rights and deadline_types]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2487. Reimplement authorization_helper.rb]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2488 Reimplementation of Add TA to course]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2489. Create a UI for Assignment Edit page &amp;quot;Etc&amp;quot; tab in ReactJS]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2490.1 Improving Assignment Participants Management UI in Expertiza]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2491.UI for View assignments in Courses view]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2492. UI for View submissions/assign grades (except heatgrid)]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2493. UI for Assign Reviewers]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - E2494. UI for Teammate Review View]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - G2401 Refactor Graphql API endpoint for contribution metrics]]&lt;br /&gt;
* [[CSC/ECE 517 Fall 2024 - G2402 Refactor Graphql API endpoint for repositories]]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160771</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160771"/>
		<updated>2024-12-04T06:08:49Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* API Documentation and Testing with Swagger */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
The controller is now fully commented, and this wiki page goes more in-depth about the code, design, and testing.&lt;br /&gt;
&lt;br /&gt;
Various other QoL changes are implemented, such as alphabetized hash parameters and collapsible source code views.&lt;br /&gt;
&lt;br /&gt;
The RSpec file is still under construction.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required: (skip steps 3 and 4 if automatic sign up is turned off)&lt;br /&gt;
## Mark the suggestion as approved&lt;br /&gt;
## Create a new topic from the suggestion&lt;br /&gt;
## Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
## Send the topic approved message&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Action &lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Method name&lt;br /&gt;
! Comment&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; | no change &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | add_comment&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve&lt;br /&gt;
| assimilated approve_suggestion&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | show&lt;br /&gt;
| assimilated student_view&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;5&amp;quot; | added &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create_topic_from_suggestion!&lt;br /&gt;
| chain of helper methods distilled into single call to create&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_student&lt;br /&gt;
| privilege check helper method, only TA and higher can pass&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_non_owner_student&lt;br /&gt;
| privilege check helper method, same as above, but student passes if he is the owner&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | destroy&lt;br /&gt;
| it should be possible to delete a suggestion (D in CRUD)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | send_notice_of_rejection&lt;br /&gt;
| complementary message to approval message&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | removed &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | action_allowed?&lt;br /&gt;
| different methods require different authorization checks, use dedicated helpers instead&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve_suggestion&lt;br /&gt;
| merged into approve&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | new&lt;br /&gt;
| view methods don't apply to JSON APIs&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_edit&lt;br /&gt;
| editing a suggestion is not allowed&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_view&lt;br /&gt;
| merged into show&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | submit&lt;br /&gt;
| call-this-or-that method&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | suggestion_params&lt;br /&gt;
| inlined into before_action declaration&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;6&amp;quot; | renamed &lt;br /&gt;
! Old&lt;br /&gt;
! New&lt;br /&gt;
!&lt;br /&gt;
|-&lt;br /&gt;
| list&lt;br /&gt;
| index&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| notification&lt;br /&gt;
| sign_team_up!&lt;br /&gt;
| method name and implementation didn't match&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion&lt;br /&gt;
| reject&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| send_email&lt;br /&gt;
| send_notice_of_approval&lt;br /&gt;
| method names should be specific&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion&lt;br /&gt;
| update&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code. Additionally, the suggestions controller now notifies the suggester if his suggestion has been rejected instead of just silently updating the state.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
This task specifically targeted the view files. Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
Even so, DRY was observed throughout the project by extracting common statements into helper functions, most notably the privilege check methods.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
===Tables===&lt;br /&gt;
&lt;br /&gt;
The following tables are newly added to the database schema. The question mark at the end of foreign_key indicates that this field is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:tables.png]]&lt;br /&gt;
&lt;br /&gt;
===Model associations===&lt;br /&gt;
&lt;br /&gt;
While simple at first glance, the suggestions controller interacts with many models in the system.&lt;br /&gt;
&lt;br /&gt;
The models in the first row of the diagram are the direct subject material of the controller as most API endpoints perform CRUD operations on only these two. The grayed out models in the second row are only ever read by the controller, they can be considered static throughout any call to the controller. The remaining four models in the last row and column of the diagram are handled by the suggestion approval process. The controller may perform any of the CRUD operations on these models.&lt;br /&gt;
&lt;br /&gt;
The arrows in the diagram point in the direction of ownership, that is, an arrow points from models A to B if A belongs to B. The dashed arrow labeled 'optional' in the legend means that the foreign key is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:ActiveRecord Associations.png]]&lt;br /&gt;
&lt;br /&gt;
===Controller and collaborators===&lt;br /&gt;
&lt;br /&gt;
The following diagram shows the controller, its API endpoints, its collaborators, and the methods accessed from the controller. The AuthorizationHelper module is imported into the controller class.&lt;br /&gt;
&lt;br /&gt;
[[File:classes.png]]&lt;br /&gt;
&lt;br /&gt;
==JSON API==&lt;br /&gt;
&lt;br /&gt;
The backend reimplementation project aims to convert Expertiza into a split frontend-backend service. To this end, the backend will respond with JSON only.&lt;br /&gt;
&lt;br /&gt;
===Request Format===&lt;br /&gt;
&lt;br /&gt;
In addition to the authorization headers, a combination of seven fields may be required for the specific API endpoint. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! rowspan=&amp;quot;2&amp;quot; | Parameter !! colspan=&amp;quot;8&amp;quot; | Included in API Endpoint !! rowspan=&amp;quot;2&amp;quot; | Type !! rowspan=&amp;quot;2&amp;quot; | Description&lt;br /&gt;
|-&lt;br /&gt;
! add_comment !! approve !! create !! destroy !! index !! reject !! show !! update&lt;br /&gt;
|-&lt;br /&gt;
! id&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓* || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || integer || Suggestion ID&lt;br /&gt;
|-&lt;br /&gt;
! anonymous&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || boolean || Whether to nullify user ID of suggester&lt;br /&gt;
|-&lt;br /&gt;
! assignment_id&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || integer || Assignment ID&lt;br /&gt;
|-&lt;br /&gt;
! auto_signup&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || boolean || Whether to sign up team when approved&lt;br /&gt;
|-&lt;br /&gt;
! comment&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || string || SuggestionComment content&lt;br /&gt;
|-&lt;br /&gt;
! description&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion description&lt;br /&gt;
|-&lt;br /&gt;
! title&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion title&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
* *: The id parameter specifies the Assignment ID instead&lt;br /&gt;
* ✓: Field required&lt;br /&gt;
* ✕: Field ignored&lt;br /&gt;
* ~: Field optional&lt;br /&gt;
&lt;br /&gt;
For example, the request data for the add_comment method would be { &amp;quot;id&amp;quot;: 1, &amp;quot;comment&amp;quot;: &amp;quot;This is a comment on one of the suggestions&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
===Status Codes===&lt;br /&gt;
&lt;br /&gt;
Depending on the parameters and database records, different HTTP status codes may be returned. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Code || Mnemonic || Condition || API Endpoints&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 2xx || success || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 200 || ok || Request successful || approve&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&amp;lt;br&amp;gt;(c&amp;lt;strong&amp;gt;RU&amp;lt;/strong&amp;gt;d)&lt;br /&gt;
|-&lt;br /&gt;
| 201 || created || Comment successfully created&amp;lt;br&amp;gt;SuggestionComment successfully created || add_comment&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;(&amp;lt;strong&amp;gt;C&amp;lt;/strong&amp;gt;rud)&lt;br /&gt;
|-&lt;br /&gt;
| 204 || no content || Suggestion successfully deleted || delete&amp;lt;br&amp;gt;(cru&amp;lt;strong&amp;gt;D&amp;lt;/strong&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 4xx || client error || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 403 || forbidden || Privilege check fails || approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 404 || not found || ActiveRecord::RecordNotFound exception || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 422 || unprocessable entity || ActiveRecord::RecordInvalid exception&amp;lt;br&amp;gt;ActiveRecord::RecordNotDestroyed exception&amp;lt;br&amp;gt;Suggestion already approved when trying to reject || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;update&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Response Format===&lt;br /&gt;
&lt;br /&gt;
Depending on the API endpoint of the request, a different entity may be returned. Note that only a success (2xx) will return an entity, a client error (4xx) will return an error message instead. The returned entities are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! API Endpoint || Rendered Model&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || SuggestionComment&lt;br /&gt;
|-&lt;br /&gt;
| approve || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| create || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| destroy || { }&lt;br /&gt;
|-&lt;br /&gt;
| index || Array of Suggestions&lt;br /&gt;
|-&lt;br /&gt;
| reject || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| show || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| update || Suggestion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
Each of the boxes with file names are expandable and contain the contents of the file. If applicable, a before-after view is provided.&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :title, :description, presence: true&lt;br /&gt;
  has_many :suggestion_comments&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :assignment&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
# https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  # Strong params inlined as global before_action&lt;br /&gt;
  before_action except: [:index] do&lt;br /&gt;
    params.require(:id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Comment on a suggestion.&lt;br /&gt;
  # A new SuggestionComment record is made.&lt;br /&gt;
  def add_comment&lt;br /&gt;
    params.require(:comment)&lt;br /&gt;
    Suggestion.find(params[:id])&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Approve a suggestion, even if it was previously rejected.&lt;br /&gt;
  def approve&lt;br /&gt;
    deny_student('Students cannot approve a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Only go through the approval process if the suggestion isn't already approved.&lt;br /&gt;
    unless @suggestion.status == 'Approved'&lt;br /&gt;
      # Since this process updates multiple records at once, wrap them in&lt;br /&gt;
      #   a transaction so that either all of them succeed, or none of them.&lt;br /&gt;
      transaction do&lt;br /&gt;
        # The approval process is:&lt;br /&gt;
        # 1. Mark the suggestion as approved&lt;br /&gt;
        # 2. Create a new topic from the suggestion&lt;br /&gt;
        # 3. Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
        # 4. Send the topic approved message&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        # If a suggestion is anonymous, the suggester is&lt;br /&gt;
        #  unknown and thus steps 3 and 4 can't be taken.&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up!&lt;br /&gt;
          send_notice_of_approval&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # A new Suggestion record is made.&lt;br /&gt;
  def create&lt;br /&gt;
    params.require(%i[anonymous assignment_id auto_signup comment description title])&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      # Anonymous suggestions are allowed by nulling user_id.&lt;br /&gt;
      user_id: params[:anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Delete a suggestion from the records.&lt;br /&gt;
  def destroy&lt;br /&gt;
    deny_student('Students cannot delete suggestions.')&lt;br /&gt;
    Suggestion.find(params[:id]).destroy!&lt;br /&gt;
    render json: {}, status: :no_content # 204&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a list of all Suggestion records associated with a particular assignment.&lt;br /&gt;
  def index&lt;br /&gt;
    deny_student('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
    render json: Suggestion.where(assignment_id: params[:id]), status: :ok # 200&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Reject a suggestion unless it was already approved.&lt;br /&gt;
  def reject&lt;br /&gt;
    deny_student('Students cannot reject a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Since the approval process makes changes to many records,&lt;br /&gt;
    #   rejecting a previously approved suggestion is not possible.&lt;br /&gt;
    if @suggestion.status == 'Approved'&lt;br /&gt;
      render json: { error: 'Suggestion has already been approved.' }, status: :unprocessable_entity # 422&lt;br /&gt;
    elsif @suggestion.status == 'Initialized'&lt;br /&gt;
      # The rejection process is:&lt;br /&gt;
      # 1. Mark the suggestion as rejected&lt;br /&gt;
      # 2. Send the topic rejected message&lt;br /&gt;
      @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      send_notice_of_rejection if @suggestion.user_id&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a single Suggestion record.&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only view their own suggestions.')&lt;br /&gt;
    render json: {&lt;br /&gt;
      comments: SuggestionComment.where(suggestion_id: params[:id]),&lt;br /&gt;
      suggestion: @suggestion&lt;br /&gt;
    }, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Change the details of a Suggestion.&lt;br /&gt;
  def update&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only edit their own suggestions.')&lt;br /&gt;
    # Only title, description, and signup preference can be changed.&lt;br /&gt;
    @suggestion.update!(params.permit(:title, :description, :auto_signup))&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    # Convert a suggestion into a fully fledged topic.&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1,&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_student(err_msg)&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_non_owner_student(err_msg)&lt;br /&gt;
    # If the student owns the resource, they are allowed to perform&lt;br /&gt;
    #   every action related to Suggestions and SuggestionComments.&lt;br /&gt;
    return if @suggestion.user_id == @current_user.id&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval&lt;br /&gt;
    # Email the suggester and CC the suggester's teammates that the suggestion was approved&lt;br /&gt;
    Mailer.send_topic_approved_email(&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      suggester: @suggester,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_rejection&lt;br /&gt;
    # Email the suggester that the suggestion was rejected&lt;br /&gt;
    Mailer.send_topic_rejected_email(&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been rejected&amp;quot;,&lt;br /&gt;
      suggester: User.find(@suggestion.user_id),&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    # Find the suggester's team which is signed up to the topic's assignment.&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    # Create the team if necessary.&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    # If the suggester's team has no assignment, then clear its&lt;br /&gt;
    #   waitlist and sign it up to the newly created topic.&lt;br /&gt;
    unless SignedUpTeam.exists?(team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: true).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    # Since the suggester's team is signed up to the topic, make it private&lt;br /&gt;
    #   to the suggester so no other teams will attempt to sign up to it.&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML.&amp;lt;br&amp;gt;The JSON API is described in [[#JSON API|JSON API]]&lt;br /&gt;
* Switched from check-avoid to fail-recover coding style to minimize number of if statements and thus indentation.&amp;lt;br&amp;gt;As a result, the methods become easier to understand, as the intended behavior is programmed all in one block with each error handled all the way at the end of the method&lt;br /&gt;
* Methods have proper error handling.&amp;lt;br&amp;gt;Each method was given a 'rescue' clause for any error that might occur, and most ActiveRecords methods were switched from the ones that return a boolean to the ones that throw an exception&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects and end in an exclamation point if the method modifies the database&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-segmented into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries.&amp;lt;br&amp;gt;Additionally, a complementary email method for rejections has been addded&lt;br /&gt;
* Where required, API entry points call dedicated privilege check methods&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, cc: defn[:cc], subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_topic_rejected_email(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;authorization_helper.rb (Trimmed)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (authorization_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_approved_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been approved.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you want to work on this topic, you can log into Expertiza and switch topics.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_rejected_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been rejected.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you believe this to be an error, please contact your instructor or one of the TAs.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The authorization helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
The mailer helper is accompanied by its template files for each type of message sent, in this case the topic approved and topic rejected messages. They also exist as plain text template files, since the original repo had both html and plaintext versions of the email templates. The plaintext versions of the template files are not shown as their content is the same, just stripped of the HTML tags.&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the following section:&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (show test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  get 'show suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when user is a instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
      response '200', 'suggestion details and comments returned' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
          expect(data['comments']).to be_an(Array)&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is owner of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        # make sure user is set as owner of suggestion&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
        # test for student being able to view their own suggestion&lt;br /&gt;
        response '200', 'student can view their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is not owner of suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
        response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for when suggestion doesn't exist&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (add comment test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
  post 'Add a comment to a suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
    parameter name: :comment, in: :body, schema: {&lt;br /&gt;
      type: :object,&lt;br /&gt;
      properties: {&lt;br /&gt;
        comment: { type: :string }&lt;br /&gt;
      },&lt;br /&gt;
      required: ['comment']&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # successful comment addition test&lt;br /&gt;
    response '201', 'comment_added' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for missing or empty comment&lt;br /&gt;
    response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        # Mock the params to simulate the request&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
        # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
      let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (approve test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
  post 'Approve suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # successful suggestion approval&lt;br /&gt;
      response '200', 'suggestion approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Approved')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable with a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable without a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when a suggestion is not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user as not having ta_privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for students not being able to approve suggestions&lt;br /&gt;
      response '403', 'students cannot approve suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (reject test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
  post 'Reject suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is instructor/ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion rejection&lt;br /&gt;
      response '200', 'suggestion rejected' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Rejected')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion already being approved&lt;br /&gt;
      response '422', 'suggestion already approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulate suggestion status as 'Approved'&lt;br /&gt;
          suggestion.update!(status: 'Approved')&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          parsed_response = JSON.parse(response.body)&lt;br /&gt;
          expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when the suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
      response '403', 'students cannot reject suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (edit/update test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  patch 'update suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion update&lt;br /&gt;
      response '200', 'suggestion updated' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to be a student and setup current user&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is owner/a part of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students can update their own suggestion(s)&lt;br /&gt;
        response '200', 'student can update their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is not owner to suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
        response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not being found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion being invalid in some form/missing&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
        allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (create test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  post 'Create suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
              description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
    # test for successful suggestion creation&lt;br /&gt;
    response '201', 'suggestion created successfully' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Suggestion title',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return properly for test&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
        expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
        expect(data['auto_signup']).to eq(true)&lt;br /&gt;
        expect(data['status']).to eq('Initialized')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for a missing parameter&lt;br /&gt;
    response '422', 'missing title' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: '',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return current user properly&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['error']).to eq('title is missing')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for invalid suggestion given&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Sample Suggestion',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
        allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  delete 'Delete suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion deletion&lt;br /&gt;
      response '204', 'suggestion deleted' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(204)&lt;br /&gt;
          expect(response.body).to be_empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for record not being destroyed&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the deletion process&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion is not found/doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test to make sure students cannot delete suggestions&lt;br /&gt;
      response '403', 'students cannot delete suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (index test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  get 'list all suggestions' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful indexing&lt;br /&gt;
      response '200', 'suggestions listed' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
         run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
          expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test case for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for student to be forbidden from indexing suggestions&lt;br /&gt;
      response '403', 'students cannot index suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== API Documentation and Testing with Swagger ===&lt;br /&gt;
Allows for quick and easy understanding and testing of API through interactive documentation.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Setup:&amp;lt;/b&amp;gt; Integrate Swagger with Rails application using gems like rswag.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Documentation:&amp;lt;/b&amp;gt; Annotate the testing code so that Swagger can automatically generate documentation. Make sure to describe each parameter and requirement, give details on how responses are structured and ensure that the endpoints are described nicely. The Swagger UI also allows for interactive testing, allowing for easy testing of the API even without a good understanding of the technical know-how on how to do so.&lt;br /&gt;
&lt;br /&gt;
[[File: SwaggerDocumentationSuggestionsController.png]]&lt;br /&gt;
&lt;br /&gt;
=== Updated Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def login_user&lt;br /&gt;
  # Create a user using the factory&lt;br /&gt;
  user = create(:user)&lt;br /&gt;
&lt;br /&gt;
  # Make a POST request to login&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
&lt;br /&gt;
  # Parse the JSON response and extract the token&lt;br /&gt;
  json_response = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
  # Return the token from the response&lt;br /&gt;
  { token: json_response['token'], user: }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  # Login user, grab token, set user and current user&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    auth_data = login_user&lt;br /&gt;
    @token = auth_data[:token]&lt;br /&gt;
    @user = auth_data[:user]&lt;br /&gt;
    @current_user = @user&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # create default assignment and suggestion&lt;br /&gt;
  let(:assignment) { create(:assignment) }&lt;br /&gt;
  let(:suggestion) { create(:suggestion, assignment_id: assignment.id, user_id: @user.id) }&lt;br /&gt;
&lt;br /&gt;
  # Testing for add_comment method&lt;br /&gt;
  path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
    post 'Add a comment to a suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
      parameter name: :comment, in: :body, schema: {&lt;br /&gt;
        type: :object,&lt;br /&gt;
        properties: {&lt;br /&gt;
          comment: { type: :string }&lt;br /&gt;
        },&lt;br /&gt;
        required: ['comment']&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      # successful comment addition test&lt;br /&gt;
      response '201', 'comment_added' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for missing or empty comment&lt;br /&gt;
      response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Mock the params to simulate the request&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
          # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
        let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Tests for approving suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
    post 'Approve suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # successful suggestion approval&lt;br /&gt;
        response '200', 'suggestion approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Approved')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable with a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable without a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when a suggestion is not found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user as not having ta_privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for students not being able to approve suggestions&lt;br /&gt;
        response '403', 'students cannot approve suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for creating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    post 'Create suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
                description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion creation&lt;br /&gt;
      response '201', 'suggestion created successfully' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Suggestion title',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return properly for test&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
          expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
          expect(data['auto_signup']).to eq(true)&lt;br /&gt;
          expect(data['status']).to eq('Initialized')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for a missing parameter&lt;br /&gt;
      response '422', 'missing title' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: '',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return current user properly&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['error']).to eq('title is missing')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for invalid suggestion given&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Sample Suggestion',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
          suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
          allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for deleting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    delete 'Delete suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion deletion&lt;br /&gt;
        response '204', 'suggestion deleted' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(204)&lt;br /&gt;
            expect(response.body).to be_empty&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for record not being destroyed&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the deletion process&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when suggestion is not found/doesn't exist&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students cannot delete suggestions&lt;br /&gt;
        response '403', 'students cannot delete suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for indexing/listing all suggestions&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    get 'list all suggestions' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
          suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful indexing&lt;br /&gt;
        response '200', 'suggestions listed' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
            expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for student to be forbidden from indexing suggestions&lt;br /&gt;
        response '403', 'students cannot index suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for rejecting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
    post 'Reject suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is instructor/ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion rejection&lt;br /&gt;
        response '200', 'suggestion rejected' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Rejected')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for suggestion already being approved&lt;br /&gt;
        response '422', 'suggestion already approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulate suggestion status as 'Approved'&lt;br /&gt;
            suggestion.update!(status: 'Approved')&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
            parsed_response = JSON.parse(response.body)&lt;br /&gt;
            expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when the suggestion not being found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
        response '403', 'students cannot reject suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for showing suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    get 'show suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when user is a instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
        response '200', 'suggestion details and comments returned' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when user is owner of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          # make sure user is set as owner of suggestion&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
          # test for student being able to view their own suggestion&lt;br /&gt;
          response '200', 'student can view their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              data = JSON.parse(response.body)&lt;br /&gt;
              expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
              expect(data['comments']).to be_an(Array)&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test case for when user is not owner of suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
          response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for updating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    patch 'update suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion update&lt;br /&gt;
        response '200', 'suggestion updated' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to be a student and setup current user&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is owner/a part of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test to make sure students can update their own suggestion(s)&lt;br /&gt;
          response '200', 'student can update their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is not owner to suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
          response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion being invalid in some form/missing&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
          allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Test Coverage===&lt;br /&gt;
[[File:TestCoverageSuggestionsController.png]]&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add detail to testing&lt;br /&gt;
* Record testing video&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Pranavi Sanganabhatla (psangan@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
* [# Testing video]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160769</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160769"/>
		<updated>2024-12-04T06:03:54Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Test Plan */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
The controller is now fully commented, and this wiki page goes more in-depth about the code, design, and testing.&lt;br /&gt;
&lt;br /&gt;
Various other QoL changes are implemented, such as alphabetized hash parameters and collapsible source code views.&lt;br /&gt;
&lt;br /&gt;
The RSpec file is still under construction.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required: (skip steps 3 and 4 if automatic sign up is turned off)&lt;br /&gt;
## Mark the suggestion as approved&lt;br /&gt;
## Create a new topic from the suggestion&lt;br /&gt;
## Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
## Send the topic approved message&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Action &lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Method name&lt;br /&gt;
! Comment&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; | no change &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | add_comment&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve&lt;br /&gt;
| assimilated approve_suggestion&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | show&lt;br /&gt;
| assimilated student_view&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;5&amp;quot; | added &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create_topic_from_suggestion!&lt;br /&gt;
| chain of helper methods distilled into single call to create&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_student&lt;br /&gt;
| privilege check helper method, only TA and higher can pass&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_non_owner_student&lt;br /&gt;
| privilege check helper method, same as above, but student passes if he is the owner&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | destroy&lt;br /&gt;
| it should be possible to delete a suggestion (D in CRUD)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | send_notice_of_rejection&lt;br /&gt;
| complementary message to approval message&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | removed &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | action_allowed?&lt;br /&gt;
| different methods require different authorization checks, use dedicated helpers instead&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve_suggestion&lt;br /&gt;
| merged into approve&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | new&lt;br /&gt;
| view methods don't apply to JSON APIs&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_edit&lt;br /&gt;
| editing a suggestion is not allowed&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_view&lt;br /&gt;
| merged into show&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | submit&lt;br /&gt;
| call-this-or-that method&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | suggestion_params&lt;br /&gt;
| inlined into before_action declaration&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;6&amp;quot; | renamed &lt;br /&gt;
! Old&lt;br /&gt;
! New&lt;br /&gt;
!&lt;br /&gt;
|-&lt;br /&gt;
| list&lt;br /&gt;
| index&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| notification&lt;br /&gt;
| sign_team_up!&lt;br /&gt;
| method name and implementation didn't match&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion&lt;br /&gt;
| reject&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| send_email&lt;br /&gt;
| send_notice_of_approval&lt;br /&gt;
| method names should be specific&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion&lt;br /&gt;
| update&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code. Additionally, the suggestions controller now notifies the suggester if his suggestion has been rejected instead of just silently updating the state.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
This task specifically targeted the view files. Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
Even so, DRY was observed throughout the project by extracting common statements into helper functions, most notably the privilege check methods.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
===Tables===&lt;br /&gt;
&lt;br /&gt;
The following tables are newly added to the database schema. The question mark at the end of foreign_key indicates that this field is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:tables.png]]&lt;br /&gt;
&lt;br /&gt;
===Model associations===&lt;br /&gt;
&lt;br /&gt;
While simple at first glance, the suggestions controller interacts with many models in the system.&lt;br /&gt;
&lt;br /&gt;
The models in the first row of the diagram are the direct subject material of the controller as most API endpoints perform CRUD operations on only these two. The grayed out models in the second row are only ever read by the controller, they can be considered static throughout any call to the controller. The remaining four models in the last row and column of the diagram are handled by the suggestion approval process. The controller may perform any of the CRUD operations on these models.&lt;br /&gt;
&lt;br /&gt;
The arrows in the diagram point in the direction of ownership, that is, an arrow points from models A to B if A belongs to B. The dashed arrow labeled 'optional' in the legend means that the foreign key is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:ActiveRecord Associations.png]]&lt;br /&gt;
&lt;br /&gt;
===Controller and collaborators===&lt;br /&gt;
&lt;br /&gt;
The following diagram shows the controller, its API endpoints, its collaborators, and the methods accessed from the controller. The AuthorizationHelper module is imported into the controller class.&lt;br /&gt;
&lt;br /&gt;
[[File:classes.png]]&lt;br /&gt;
&lt;br /&gt;
==JSON API==&lt;br /&gt;
&lt;br /&gt;
The backend reimplementation project aims to convert Expertiza into a split frontend-backend service. To this end, the backend will respond with JSON only.&lt;br /&gt;
&lt;br /&gt;
===Request Format===&lt;br /&gt;
&lt;br /&gt;
In addition to the authorization headers, a combination of seven fields may be required for the specific API endpoint. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! rowspan=&amp;quot;2&amp;quot; | Parameter !! colspan=&amp;quot;8&amp;quot; | Included in API Endpoint !! rowspan=&amp;quot;2&amp;quot; | Type !! rowspan=&amp;quot;2&amp;quot; | Description&lt;br /&gt;
|-&lt;br /&gt;
! add_comment !! approve !! create !! destroy !! index !! reject !! show !! update&lt;br /&gt;
|-&lt;br /&gt;
! id&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓* || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || integer || Suggestion ID&lt;br /&gt;
|-&lt;br /&gt;
! anonymous&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || boolean || Whether to nullify user ID of suggester&lt;br /&gt;
|-&lt;br /&gt;
! assignment_id&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || integer || Assignment ID&lt;br /&gt;
|-&lt;br /&gt;
! auto_signup&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || boolean || Whether to sign up team when approved&lt;br /&gt;
|-&lt;br /&gt;
! comment&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || string || SuggestionComment content&lt;br /&gt;
|-&lt;br /&gt;
! description&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion description&lt;br /&gt;
|-&lt;br /&gt;
! title&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion title&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
* *: The id parameter specifies the Assignment ID instead&lt;br /&gt;
* ✓: Field required&lt;br /&gt;
* ✕: Field ignored&lt;br /&gt;
* ~: Field optional&lt;br /&gt;
&lt;br /&gt;
For example, the request data for the add_comment method would be { &amp;quot;id&amp;quot;: 1, &amp;quot;comment&amp;quot;: &amp;quot;This is a comment on one of the suggestions&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
===Status Codes===&lt;br /&gt;
&lt;br /&gt;
Depending on the parameters and database records, different HTTP status codes may be returned. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Code || Mnemonic || Condition || API Endpoints&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 2xx || success || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 200 || ok || Request successful || approve&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&amp;lt;br&amp;gt;(c&amp;lt;strong&amp;gt;RU&amp;lt;/strong&amp;gt;d)&lt;br /&gt;
|-&lt;br /&gt;
| 201 || created || Comment successfully created&amp;lt;br&amp;gt;SuggestionComment successfully created || add_comment&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;(&amp;lt;strong&amp;gt;C&amp;lt;/strong&amp;gt;rud)&lt;br /&gt;
|-&lt;br /&gt;
| 204 || no content || Suggestion successfully deleted || delete&amp;lt;br&amp;gt;(cru&amp;lt;strong&amp;gt;D&amp;lt;/strong&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 4xx || client error || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 403 || forbidden || Privilege check fails || approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 404 || not found || ActiveRecord::RecordNotFound exception || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 422 || unprocessable entity || ActiveRecord::RecordInvalid exception&amp;lt;br&amp;gt;ActiveRecord::RecordNotDestroyed exception&amp;lt;br&amp;gt;Suggestion already approved when trying to reject || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;update&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Response Format===&lt;br /&gt;
&lt;br /&gt;
Depending on the API endpoint of the request, a different entity may be returned. Note that only a success (2xx) will return an entity, a client error (4xx) will return an error message instead. The returned entities are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! API Endpoint || Rendered Model&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || SuggestionComment&lt;br /&gt;
|-&lt;br /&gt;
| approve || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| create || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| destroy || { }&lt;br /&gt;
|-&lt;br /&gt;
| index || Array of Suggestions&lt;br /&gt;
|-&lt;br /&gt;
| reject || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| show || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| update || Suggestion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
Each of the boxes with file names are expandable and contain the contents of the file. If applicable, a before-after view is provided.&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :title, :description, presence: true&lt;br /&gt;
  has_many :suggestion_comments&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :assignment&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
# https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  # Strong params inlined as global before_action&lt;br /&gt;
  before_action except: [:index] do&lt;br /&gt;
    params.require(:id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Comment on a suggestion.&lt;br /&gt;
  # A new SuggestionComment record is made.&lt;br /&gt;
  def add_comment&lt;br /&gt;
    params.require(:comment)&lt;br /&gt;
    Suggestion.find(params[:id])&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Approve a suggestion, even if it was previously rejected.&lt;br /&gt;
  def approve&lt;br /&gt;
    deny_student('Students cannot approve a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Only go through the approval process if the suggestion isn't already approved.&lt;br /&gt;
    unless @suggestion.status == 'Approved'&lt;br /&gt;
      # Since this process updates multiple records at once, wrap them in&lt;br /&gt;
      #   a transaction so that either all of them succeed, or none of them.&lt;br /&gt;
      transaction do&lt;br /&gt;
        # The approval process is:&lt;br /&gt;
        # 1. Mark the suggestion as approved&lt;br /&gt;
        # 2. Create a new topic from the suggestion&lt;br /&gt;
        # 3. Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
        # 4. Send the topic approved message&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        # If a suggestion is anonymous, the suggester is&lt;br /&gt;
        #  unknown and thus steps 3 and 4 can't be taken.&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up!&lt;br /&gt;
          send_notice_of_approval&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # A new Suggestion record is made.&lt;br /&gt;
  def create&lt;br /&gt;
    params.require(%i[anonymous assignment_id auto_signup comment description title])&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      # Anonymous suggestions are allowed by nulling user_id.&lt;br /&gt;
      user_id: params[:anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Delete a suggestion from the records.&lt;br /&gt;
  def destroy&lt;br /&gt;
    deny_student('Students cannot delete suggestions.')&lt;br /&gt;
    Suggestion.find(params[:id]).destroy!&lt;br /&gt;
    render json: {}, status: :no_content # 204&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a list of all Suggestion records associated with a particular assignment.&lt;br /&gt;
  def index&lt;br /&gt;
    deny_student('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
    render json: Suggestion.where(assignment_id: params[:id]), status: :ok # 200&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Reject a suggestion unless it was already approved.&lt;br /&gt;
  def reject&lt;br /&gt;
    deny_student('Students cannot reject a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Since the approval process makes changes to many records,&lt;br /&gt;
    #   rejecting a previously approved suggestion is not possible.&lt;br /&gt;
    if @suggestion.status == 'Approved'&lt;br /&gt;
      render json: { error: 'Suggestion has already been approved.' }, status: :unprocessable_entity # 422&lt;br /&gt;
    elsif @suggestion.status == 'Initialized'&lt;br /&gt;
      # The rejection process is:&lt;br /&gt;
      # 1. Mark the suggestion as rejected&lt;br /&gt;
      # 2. Send the topic rejected message&lt;br /&gt;
      @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      send_notice_of_rejection if @suggestion.user_id&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a single Suggestion record.&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only view their own suggestions.')&lt;br /&gt;
    render json: {&lt;br /&gt;
      comments: SuggestionComment.where(suggestion_id: params[:id]),&lt;br /&gt;
      suggestion: @suggestion&lt;br /&gt;
    }, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Change the details of a Suggestion.&lt;br /&gt;
  def update&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only edit their own suggestions.')&lt;br /&gt;
    # Only title, description, and signup preference can be changed.&lt;br /&gt;
    @suggestion.update!(params.permit(:title, :description, :auto_signup))&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    # Convert a suggestion into a fully fledged topic.&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1,&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_student(err_msg)&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_non_owner_student(err_msg)&lt;br /&gt;
    # If the student owns the resource, they are allowed to perform&lt;br /&gt;
    #   every action related to Suggestions and SuggestionComments.&lt;br /&gt;
    return if @suggestion.user_id == @current_user.id&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval&lt;br /&gt;
    # Email the suggester and CC the suggester's teammates that the suggestion was approved&lt;br /&gt;
    Mailer.send_topic_approved_email(&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      suggester: @suggester,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_rejection&lt;br /&gt;
    # Email the suggester that the suggestion was rejected&lt;br /&gt;
    Mailer.send_topic_rejected_email(&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been rejected&amp;quot;,&lt;br /&gt;
      suggester: User.find(@suggestion.user_id),&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    # Find the suggester's team which is signed up to the topic's assignment.&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    # Create the team if necessary.&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    # If the suggester's team has no assignment, then clear its&lt;br /&gt;
    #   waitlist and sign it up to the newly created topic.&lt;br /&gt;
    unless SignedUpTeam.exists?(team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: true).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    # Since the suggester's team is signed up to the topic, make it private&lt;br /&gt;
    #   to the suggester so no other teams will attempt to sign up to it.&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML.&amp;lt;br&amp;gt;The JSON API is described in [[#JSON API|JSON API]]&lt;br /&gt;
* Switched from check-avoid to fail-recover coding style to minimize number of if statements and thus indentation.&amp;lt;br&amp;gt;As a result, the methods become easier to understand, as the intended behavior is programmed all in one block with each error handled all the way at the end of the method&lt;br /&gt;
* Methods have proper error handling.&amp;lt;br&amp;gt;Each method was given a 'rescue' clause for any error that might occur, and most ActiveRecords methods were switched from the ones that return a boolean to the ones that throw an exception&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects and end in an exclamation point if the method modifies the database&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-segmented into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries.&amp;lt;br&amp;gt;Additionally, a complementary email method for rejections has been addded&lt;br /&gt;
* Where required, API entry points call dedicated privilege check methods&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, cc: defn[:cc], subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_topic_rejected_email(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;authorization_helper.rb (Trimmed)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (authorization_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_approved_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been approved.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you want to work on this topic, you can log into Expertiza and switch topics.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_rejected_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been rejected.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you believe this to be an error, please contact your instructor or one of the TAs.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The authorization helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
The mailer helper is accompanied by its template files for each type of message sent, in this case the topic approved and topic rejected messages. They also exist as plain text template files, since the original repo had both html and plaintext versions of the email templates. The plaintext versions of the template files are not shown as their content is the same, just stripped of the HTML tags.&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the following section:&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (show test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  get 'show suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when user is a instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
      response '200', 'suggestion details and comments returned' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
          expect(data['comments']).to be_an(Array)&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is owner of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        # make sure user is set as owner of suggestion&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
        # test for student being able to view their own suggestion&lt;br /&gt;
        response '200', 'student can view their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is not owner of suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
        response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for when suggestion doesn't exist&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (add comment test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
  post 'Add a comment to a suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
    parameter name: :comment, in: :body, schema: {&lt;br /&gt;
      type: :object,&lt;br /&gt;
      properties: {&lt;br /&gt;
        comment: { type: :string }&lt;br /&gt;
      },&lt;br /&gt;
      required: ['comment']&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # successful comment addition test&lt;br /&gt;
    response '201', 'comment_added' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for missing or empty comment&lt;br /&gt;
    response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        # Mock the params to simulate the request&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
        # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
      let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (approve test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
  post 'Approve suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # successful suggestion approval&lt;br /&gt;
      response '200', 'suggestion approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Approved')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable with a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable without a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when a suggestion is not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user as not having ta_privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for students not being able to approve suggestions&lt;br /&gt;
      response '403', 'students cannot approve suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (reject test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
  post 'Reject suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is instructor/ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion rejection&lt;br /&gt;
      response '200', 'suggestion rejected' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Rejected')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion already being approved&lt;br /&gt;
      response '422', 'suggestion already approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulate suggestion status as 'Approved'&lt;br /&gt;
          suggestion.update!(status: 'Approved')&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          parsed_response = JSON.parse(response.body)&lt;br /&gt;
          expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when the suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
      response '403', 'students cannot reject suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (edit/update test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  patch 'update suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion update&lt;br /&gt;
      response '200', 'suggestion updated' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to be a student and setup current user&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is owner/a part of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students can update their own suggestion(s)&lt;br /&gt;
        response '200', 'student can update their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is not owner to suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
        response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not being found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion being invalid in some form/missing&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
        allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (create test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  post 'Create suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
              description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
    # test for successful suggestion creation&lt;br /&gt;
    response '201', 'suggestion created successfully' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Suggestion title',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return properly for test&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
        expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
        expect(data['auto_signup']).to eq(true)&lt;br /&gt;
        expect(data['status']).to eq('Initialized')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for a missing parameter&lt;br /&gt;
    response '422', 'missing title' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: '',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return current user properly&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['error']).to eq('title is missing')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for invalid suggestion given&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Sample Suggestion',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
        allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  delete 'Delete suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion deletion&lt;br /&gt;
      response '204', 'suggestion deleted' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(204)&lt;br /&gt;
          expect(response.body).to be_empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for record not being destroyed&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the deletion process&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion is not found/doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test to make sure students cannot delete suggestions&lt;br /&gt;
      response '403', 'students cannot delete suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (index test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  get 'list all suggestions' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful indexing&lt;br /&gt;
      response '200', 'suggestions listed' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
         run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
          expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test case for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for student to be forbidden from indexing suggestions&lt;br /&gt;
      response '403', 'students cannot index suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== API Documentation and Testing with Swagger ===&lt;br /&gt;
[[File: SwaggerDocumentationSuggestionsController.png]]&lt;br /&gt;
&lt;br /&gt;
=== Updated Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def login_user&lt;br /&gt;
  # Create a user using the factory&lt;br /&gt;
  user = create(:user)&lt;br /&gt;
&lt;br /&gt;
  # Make a POST request to login&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
&lt;br /&gt;
  # Parse the JSON response and extract the token&lt;br /&gt;
  json_response = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
  # Return the token from the response&lt;br /&gt;
  { token: json_response['token'], user: }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  # Login user, grab token, set user and current user&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    auth_data = login_user&lt;br /&gt;
    @token = auth_data[:token]&lt;br /&gt;
    @user = auth_data[:user]&lt;br /&gt;
    @current_user = @user&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # create default assignment and suggestion&lt;br /&gt;
  let(:assignment) { create(:assignment) }&lt;br /&gt;
  let(:suggestion) { create(:suggestion, assignment_id: assignment.id, user_id: @user.id) }&lt;br /&gt;
&lt;br /&gt;
  # Testing for add_comment method&lt;br /&gt;
  path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
    post 'Add a comment to a suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
      parameter name: :comment, in: :body, schema: {&lt;br /&gt;
        type: :object,&lt;br /&gt;
        properties: {&lt;br /&gt;
          comment: { type: :string }&lt;br /&gt;
        },&lt;br /&gt;
        required: ['comment']&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      # successful comment addition test&lt;br /&gt;
      response '201', 'comment_added' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for missing or empty comment&lt;br /&gt;
      response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Mock the params to simulate the request&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
          # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
        let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Tests for approving suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
    post 'Approve suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # successful suggestion approval&lt;br /&gt;
        response '200', 'suggestion approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Approved')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable with a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable without a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when a suggestion is not found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user as not having ta_privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for students not being able to approve suggestions&lt;br /&gt;
        response '403', 'students cannot approve suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for creating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    post 'Create suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
                description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion creation&lt;br /&gt;
      response '201', 'suggestion created successfully' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Suggestion title',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return properly for test&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
          expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
          expect(data['auto_signup']).to eq(true)&lt;br /&gt;
          expect(data['status']).to eq('Initialized')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for a missing parameter&lt;br /&gt;
      response '422', 'missing title' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: '',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return current user properly&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['error']).to eq('title is missing')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for invalid suggestion given&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Sample Suggestion',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
          suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
          allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for deleting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    delete 'Delete suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion deletion&lt;br /&gt;
        response '204', 'suggestion deleted' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(204)&lt;br /&gt;
            expect(response.body).to be_empty&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for record not being destroyed&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the deletion process&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when suggestion is not found/doesn't exist&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students cannot delete suggestions&lt;br /&gt;
        response '403', 'students cannot delete suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for indexing/listing all suggestions&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    get 'list all suggestions' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
          suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful indexing&lt;br /&gt;
        response '200', 'suggestions listed' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
            expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for student to be forbidden from indexing suggestions&lt;br /&gt;
        response '403', 'students cannot index suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for rejecting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
    post 'Reject suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is instructor/ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion rejection&lt;br /&gt;
        response '200', 'suggestion rejected' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Rejected')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for suggestion already being approved&lt;br /&gt;
        response '422', 'suggestion already approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulate suggestion status as 'Approved'&lt;br /&gt;
            suggestion.update!(status: 'Approved')&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
            parsed_response = JSON.parse(response.body)&lt;br /&gt;
            expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when the suggestion not being found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
        response '403', 'students cannot reject suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for showing suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    get 'show suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when user is a instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
        response '200', 'suggestion details and comments returned' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when user is owner of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          # make sure user is set as owner of suggestion&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
          # test for student being able to view their own suggestion&lt;br /&gt;
          response '200', 'student can view their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              data = JSON.parse(response.body)&lt;br /&gt;
              expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
              expect(data['comments']).to be_an(Array)&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test case for when user is not owner of suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
          response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for updating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    patch 'update suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion update&lt;br /&gt;
        response '200', 'suggestion updated' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to be a student and setup current user&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is owner/a part of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test to make sure students can update their own suggestion(s)&lt;br /&gt;
          response '200', 'student can update their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is not owner to suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
          response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion being invalid in some form/missing&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
          allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Test Coverage===&lt;br /&gt;
[[File:TestCoverageSuggestionsController.png]]&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add detail to testing&lt;br /&gt;
* Record testing video&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Pranavi Sanganabhatla (psangan@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
* [# Testing video]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=File:SwaggerDocumentationSuggestionsController.png&amp;diff=160768</id>
		<title>File:SwaggerDocumentationSuggestionsController.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=File:SwaggerDocumentationSuggestionsController.png&amp;diff=160768"/>
		<updated>2024-12-04T06:03:24Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160766</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160766"/>
		<updated>2024-12-04T05:54:09Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Test Coverage */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
The controller is now fully commented, and this wiki page goes more in-depth about the code, design, and testing.&lt;br /&gt;
&lt;br /&gt;
Various other QoL changes are implemented, such as alphabetized hash parameters and collapsible source code views.&lt;br /&gt;
&lt;br /&gt;
The RSpec file is still under construction.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required: (skip steps 3 and 4 if automatic sign up is turned off)&lt;br /&gt;
## Mark the suggestion as approved&lt;br /&gt;
## Create a new topic from the suggestion&lt;br /&gt;
## Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
## Send the topic approved message&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Action &lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Method name&lt;br /&gt;
! Comment&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; | no change &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | add_comment&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve&lt;br /&gt;
| assimilated approve_suggestion&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | show&lt;br /&gt;
| assimilated student_view&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;5&amp;quot; | added &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create_topic_from_suggestion!&lt;br /&gt;
| chain of helper methods distilled into single call to create&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_student&lt;br /&gt;
| privilege check helper method, only TA and higher can pass&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_non_owner_student&lt;br /&gt;
| privilege check helper method, same as above, but student passes if he is the owner&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | destroy&lt;br /&gt;
| it should be possible to delete a suggestion (D in CRUD)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | send_notice_of_rejection&lt;br /&gt;
| complementary message to approval message&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | removed &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | action_allowed?&lt;br /&gt;
| different methods require different authorization checks, use dedicated helpers instead&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve_suggestion&lt;br /&gt;
| merged into approve&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | new&lt;br /&gt;
| view methods don't apply to JSON APIs&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_edit&lt;br /&gt;
| editing a suggestion is not allowed&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_view&lt;br /&gt;
| merged into show&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | submit&lt;br /&gt;
| call-this-or-that method&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | suggestion_params&lt;br /&gt;
| inlined into before_action declaration&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;6&amp;quot; | renamed &lt;br /&gt;
! Old&lt;br /&gt;
! New&lt;br /&gt;
!&lt;br /&gt;
|-&lt;br /&gt;
| list&lt;br /&gt;
| index&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| notification&lt;br /&gt;
| sign_team_up!&lt;br /&gt;
| method name and implementation didn't match&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion&lt;br /&gt;
| reject&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| send_email&lt;br /&gt;
| send_notice_of_approval&lt;br /&gt;
| method names should be specific&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion&lt;br /&gt;
| update&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code. Additionally, the suggestions controller now notifies the suggester if his suggestion has been rejected instead of just silently updating the state.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
This task specifically targeted the view files. Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
Even so, DRY was observed throughout the project by extracting common statements into helper functions, most notably the privilege check methods.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
===Tables===&lt;br /&gt;
&lt;br /&gt;
The following tables are newly added to the database schema. The question mark at the end of foreign_key indicates that this field is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:tables.png]]&lt;br /&gt;
&lt;br /&gt;
===Model associations===&lt;br /&gt;
&lt;br /&gt;
While simple at first glance, the suggestions controller interacts with many models in the system.&lt;br /&gt;
&lt;br /&gt;
The models in the first row of the diagram are the direct subject material of the controller as most API endpoints perform CRUD operations on only these two. The grayed out models in the second row are only ever read by the controller, they can be considered static throughout any call to the controller. The remaining four models in the last row and column of the diagram are handled by the suggestion approval process. The controller may perform any of the CRUD operations on these models.&lt;br /&gt;
&lt;br /&gt;
The arrows in the diagram point in the direction of ownership, that is, an arrow points from models A to B if A belongs to B. The dashed arrow labeled 'optional' in the legend means that the foreign key is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:ActiveRecord Associations.png]]&lt;br /&gt;
&lt;br /&gt;
===Controller and collaborators===&lt;br /&gt;
&lt;br /&gt;
The following diagram shows the controller, its API endpoints, its collaborators, and the methods accessed from the controller. The AuthorizationHelper module is imported into the controller class.&lt;br /&gt;
&lt;br /&gt;
[[File:classes.png]]&lt;br /&gt;
&lt;br /&gt;
==JSON API==&lt;br /&gt;
&lt;br /&gt;
The backend reimplementation project aims to convert Expertiza into a split frontend-backend service. To this end, the backend will respond with JSON only.&lt;br /&gt;
&lt;br /&gt;
===Request Format===&lt;br /&gt;
&lt;br /&gt;
In addition to the authorization headers, a combination of seven fields may be required for the specific API endpoint. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! rowspan=&amp;quot;2&amp;quot; | Parameter !! colspan=&amp;quot;8&amp;quot; | Included in API Endpoint !! rowspan=&amp;quot;2&amp;quot; | Type !! rowspan=&amp;quot;2&amp;quot; | Description&lt;br /&gt;
|-&lt;br /&gt;
! add_comment !! approve !! create !! destroy !! index !! reject !! show !! update&lt;br /&gt;
|-&lt;br /&gt;
! id&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓* || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || integer || Suggestion ID&lt;br /&gt;
|-&lt;br /&gt;
! anonymous&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || boolean || Whether to nullify user ID of suggester&lt;br /&gt;
|-&lt;br /&gt;
! assignment_id&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || integer || Assignment ID&lt;br /&gt;
|-&lt;br /&gt;
! auto_signup&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || boolean || Whether to sign up team when approved&lt;br /&gt;
|-&lt;br /&gt;
! comment&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || string || SuggestionComment content&lt;br /&gt;
|-&lt;br /&gt;
! description&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion description&lt;br /&gt;
|-&lt;br /&gt;
! title&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion title&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
* *: The id parameter specifies the Assignment ID instead&lt;br /&gt;
* ✓: Field required&lt;br /&gt;
* ✕: Field ignored&lt;br /&gt;
* ~: Field optional&lt;br /&gt;
&lt;br /&gt;
For example, the request data for the add_comment method would be { &amp;quot;id&amp;quot;: 1, &amp;quot;comment&amp;quot;: &amp;quot;This is a comment on one of the suggestions&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
===Status Codes===&lt;br /&gt;
&lt;br /&gt;
Depending on the parameters and database records, different HTTP status codes may be returned. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Code || Mnemonic || Condition || API Endpoints&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 2xx || success || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 200 || ok || Request successful || approve&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&amp;lt;br&amp;gt;(c&amp;lt;strong&amp;gt;RU&amp;lt;/strong&amp;gt;d)&lt;br /&gt;
|-&lt;br /&gt;
| 201 || created || Comment successfully created&amp;lt;br&amp;gt;SuggestionComment successfully created || add_comment&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;(&amp;lt;strong&amp;gt;C&amp;lt;/strong&amp;gt;rud)&lt;br /&gt;
|-&lt;br /&gt;
| 204 || no content || Suggestion successfully deleted || delete&amp;lt;br&amp;gt;(cru&amp;lt;strong&amp;gt;D&amp;lt;/strong&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 4xx || client error || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 403 || forbidden || Privilege check fails || approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 404 || not found || ActiveRecord::RecordNotFound exception || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 422 || unprocessable entity || ActiveRecord::RecordInvalid exception&amp;lt;br&amp;gt;ActiveRecord::RecordNotDestroyed exception&amp;lt;br&amp;gt;Suggestion already approved when trying to reject || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;update&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Response Format===&lt;br /&gt;
&lt;br /&gt;
Depending on the API endpoint of the request, a different entity may be returned. Note that only a success (2xx) will return an entity, a client error (4xx) will return an error message instead. The returned entities are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! API Endpoint || Rendered Model&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || SuggestionComment&lt;br /&gt;
|-&lt;br /&gt;
| approve || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| create || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| destroy || { }&lt;br /&gt;
|-&lt;br /&gt;
| index || Array of Suggestions&lt;br /&gt;
|-&lt;br /&gt;
| reject || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| show || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| update || Suggestion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
Each of the boxes with file names are expandable and contain the contents of the file. If applicable, a before-after view is provided.&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :title, :description, presence: true&lt;br /&gt;
  has_many :suggestion_comments&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :assignment&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
# https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  # Strong params inlined as global before_action&lt;br /&gt;
  before_action except: [:index] do&lt;br /&gt;
    params.require(:id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Comment on a suggestion.&lt;br /&gt;
  # A new SuggestionComment record is made.&lt;br /&gt;
  def add_comment&lt;br /&gt;
    params.require(:comment)&lt;br /&gt;
    Suggestion.find(params[:id])&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Approve a suggestion, even if it was previously rejected.&lt;br /&gt;
  def approve&lt;br /&gt;
    deny_student('Students cannot approve a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Only go through the approval process if the suggestion isn't already approved.&lt;br /&gt;
    unless @suggestion.status == 'Approved'&lt;br /&gt;
      # Since this process updates multiple records at once, wrap them in&lt;br /&gt;
      #   a transaction so that either all of them succeed, or none of them.&lt;br /&gt;
      transaction do&lt;br /&gt;
        # The approval process is:&lt;br /&gt;
        # 1. Mark the suggestion as approved&lt;br /&gt;
        # 2. Create a new topic from the suggestion&lt;br /&gt;
        # 3. Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
        # 4. Send the topic approved message&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        # If a suggestion is anonymous, the suggester is&lt;br /&gt;
        #  unknown and thus steps 3 and 4 can't be taken.&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up!&lt;br /&gt;
          send_notice_of_approval&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # A new Suggestion record is made.&lt;br /&gt;
  def create&lt;br /&gt;
    params.require(%i[anonymous assignment_id auto_signup comment description title])&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      # Anonymous suggestions are allowed by nulling user_id.&lt;br /&gt;
      user_id: params[:anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Delete a suggestion from the records.&lt;br /&gt;
  def destroy&lt;br /&gt;
    deny_student('Students cannot delete suggestions.')&lt;br /&gt;
    Suggestion.find(params[:id]).destroy!&lt;br /&gt;
    render json: {}, status: :no_content # 204&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a list of all Suggestion records associated with a particular assignment.&lt;br /&gt;
  def index&lt;br /&gt;
    deny_student('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
    render json: Suggestion.where(assignment_id: params[:id]), status: :ok # 200&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Reject a suggestion unless it was already approved.&lt;br /&gt;
  def reject&lt;br /&gt;
    deny_student('Students cannot reject a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Since the approval process makes changes to many records,&lt;br /&gt;
    #   rejecting a previously approved suggestion is not possible.&lt;br /&gt;
    if @suggestion.status == 'Approved'&lt;br /&gt;
      render json: { error: 'Suggestion has already been approved.' }, status: :unprocessable_entity # 422&lt;br /&gt;
    elsif @suggestion.status == 'Initialized'&lt;br /&gt;
      # The rejection process is:&lt;br /&gt;
      # 1. Mark the suggestion as rejected&lt;br /&gt;
      # 2. Send the topic rejected message&lt;br /&gt;
      @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      send_notice_of_rejection if @suggestion.user_id&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a single Suggestion record.&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only view their own suggestions.')&lt;br /&gt;
    render json: {&lt;br /&gt;
      comments: SuggestionComment.where(suggestion_id: params[:id]),&lt;br /&gt;
      suggestion: @suggestion&lt;br /&gt;
    }, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Change the details of a Suggestion.&lt;br /&gt;
  def update&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only edit their own suggestions.')&lt;br /&gt;
    # Only title, description, and signup preference can be changed.&lt;br /&gt;
    @suggestion.update!(params.permit(:title, :description, :auto_signup))&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    # Convert a suggestion into a fully fledged topic.&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1,&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_student(err_msg)&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_non_owner_student(err_msg)&lt;br /&gt;
    # If the student owns the resource, they are allowed to perform&lt;br /&gt;
    #   every action related to Suggestions and SuggestionComments.&lt;br /&gt;
    return if @suggestion.user_id == @current_user.id&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval&lt;br /&gt;
    # Email the suggester and CC the suggester's teammates that the suggestion was approved&lt;br /&gt;
    Mailer.send_topic_approved_email(&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      suggester: @suggester,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_rejection&lt;br /&gt;
    # Email the suggester that the suggestion was rejected&lt;br /&gt;
    Mailer.send_topic_rejected_email(&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been rejected&amp;quot;,&lt;br /&gt;
      suggester: User.find(@suggestion.user_id),&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    # Find the suggester's team which is signed up to the topic's assignment.&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    # Create the team if necessary.&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    # If the suggester's team has no assignment, then clear its&lt;br /&gt;
    #   waitlist and sign it up to the newly created topic.&lt;br /&gt;
    unless SignedUpTeam.exists?(team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: true).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    # Since the suggester's team is signed up to the topic, make it private&lt;br /&gt;
    #   to the suggester so no other teams will attempt to sign up to it.&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML.&amp;lt;br&amp;gt;The JSON API is described in [[#JSON API|JSON API]]&lt;br /&gt;
* Switched from check-avoid to fail-recover coding style to minimize number of if statements and thus indentation.&amp;lt;br&amp;gt;As a result, the methods become easier to understand, as the intended behavior is programmed all in one block with each error handled all the way at the end of the method&lt;br /&gt;
* Methods have proper error handling.&amp;lt;br&amp;gt;Each method was given a 'rescue' clause for any error that might occur, and most ActiveRecords methods were switched from the ones that return a boolean to the ones that throw an exception&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects and end in an exclamation point if the method modifies the database&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-segmented into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries.&amp;lt;br&amp;gt;Additionally, a complementary email method for rejections has been addded&lt;br /&gt;
* Where required, API entry points call dedicated privilege check methods&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, cc: defn[:cc], subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_topic_rejected_email(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;authorization_helper.rb (Trimmed)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (authorization_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_approved_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been approved.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you want to work on this topic, you can log into Expertiza and switch topics.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_rejected_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been rejected.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you believe this to be an error, please contact your instructor or one of the TAs.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The authorization helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
The mailer helper is accompanied by its template files for each type of message sent, in this case the topic approved and topic rejected messages. They also exist as plain text template files, since the original repo had both html and plaintext versions of the email templates. The plaintext versions of the template files are not shown as their content is the same, just stripped of the HTML tags.&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the following section:&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (show test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  get 'show suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when user is a instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
      response '200', 'suggestion details and comments returned' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
          expect(data['comments']).to be_an(Array)&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is owner of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        # make sure user is set as owner of suggestion&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
        # test for student being able to view their own suggestion&lt;br /&gt;
        response '200', 'student can view their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is not owner of suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
        response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for when suggestion doesn't exist&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (add comment test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
  post 'Add a comment to a suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
    parameter name: :comment, in: :body, schema: {&lt;br /&gt;
      type: :object,&lt;br /&gt;
      properties: {&lt;br /&gt;
        comment: { type: :string }&lt;br /&gt;
      },&lt;br /&gt;
      required: ['comment']&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # successful comment addition test&lt;br /&gt;
    response '201', 'comment_added' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for missing or empty comment&lt;br /&gt;
    response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        # Mock the params to simulate the request&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
        # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
      let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (approve test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
  post 'Approve suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # successful suggestion approval&lt;br /&gt;
      response '200', 'suggestion approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Approved')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable with a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable without a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when a suggestion is not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user as not having ta_privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for students not being able to approve suggestions&lt;br /&gt;
      response '403', 'students cannot approve suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (reject test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
  post 'Reject suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is instructor/ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion rejection&lt;br /&gt;
      response '200', 'suggestion rejected' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Rejected')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion already being approved&lt;br /&gt;
      response '422', 'suggestion already approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulate suggestion status as 'Approved'&lt;br /&gt;
          suggestion.update!(status: 'Approved')&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          parsed_response = JSON.parse(response.body)&lt;br /&gt;
          expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when the suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
      response '403', 'students cannot reject suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (edit/update test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  patch 'update suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion update&lt;br /&gt;
      response '200', 'suggestion updated' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to be a student and setup current user&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is owner/a part of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students can update their own suggestion(s)&lt;br /&gt;
        response '200', 'student can update their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is not owner to suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
        response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not being found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion being invalid in some form/missing&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
        allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (create test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  post 'Create suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
              description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
    # test for successful suggestion creation&lt;br /&gt;
    response '201', 'suggestion created successfully' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Suggestion title',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return properly for test&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
        expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
        expect(data['auto_signup']).to eq(true)&lt;br /&gt;
        expect(data['status']).to eq('Initialized')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for a missing parameter&lt;br /&gt;
    response '422', 'missing title' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: '',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return current user properly&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['error']).to eq('title is missing')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for invalid suggestion given&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Sample Suggestion',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
        allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  delete 'Delete suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion deletion&lt;br /&gt;
      response '204', 'suggestion deleted' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(204)&lt;br /&gt;
          expect(response.body).to be_empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for record not being destroyed&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the deletion process&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion is not found/doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test to make sure students cannot delete suggestions&lt;br /&gt;
      response '403', 'students cannot delete suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (index test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  get 'list all suggestions' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful indexing&lt;br /&gt;
      response '200', 'suggestions listed' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
         run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
          expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test case for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for student to be forbidden from indexing suggestions&lt;br /&gt;
      response '403', 'students cannot index suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Updated Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def login_user&lt;br /&gt;
  # Create a user using the factory&lt;br /&gt;
  user = create(:user)&lt;br /&gt;
&lt;br /&gt;
  # Make a POST request to login&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
&lt;br /&gt;
  # Parse the JSON response and extract the token&lt;br /&gt;
  json_response = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
  # Return the token from the response&lt;br /&gt;
  { token: json_response['token'], user: }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  # Login user, grab token, set user and current user&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    auth_data = login_user&lt;br /&gt;
    @token = auth_data[:token]&lt;br /&gt;
    @user = auth_data[:user]&lt;br /&gt;
    @current_user = @user&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # create default assignment and suggestion&lt;br /&gt;
  let(:assignment) { create(:assignment) }&lt;br /&gt;
  let(:suggestion) { create(:suggestion, assignment_id: assignment.id, user_id: @user.id) }&lt;br /&gt;
&lt;br /&gt;
  # Testing for add_comment method&lt;br /&gt;
  path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
    post 'Add a comment to a suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
      parameter name: :comment, in: :body, schema: {&lt;br /&gt;
        type: :object,&lt;br /&gt;
        properties: {&lt;br /&gt;
          comment: { type: :string }&lt;br /&gt;
        },&lt;br /&gt;
        required: ['comment']&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      # successful comment addition test&lt;br /&gt;
      response '201', 'comment_added' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for missing or empty comment&lt;br /&gt;
      response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Mock the params to simulate the request&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
          # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
        let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Tests for approving suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
    post 'Approve suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # successful suggestion approval&lt;br /&gt;
        response '200', 'suggestion approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Approved')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable with a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable without a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when a suggestion is not found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user as not having ta_privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for students not being able to approve suggestions&lt;br /&gt;
        response '403', 'students cannot approve suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for creating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    post 'Create suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
                description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion creation&lt;br /&gt;
      response '201', 'suggestion created successfully' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Suggestion title',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return properly for test&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
          expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
          expect(data['auto_signup']).to eq(true)&lt;br /&gt;
          expect(data['status']).to eq('Initialized')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for a missing parameter&lt;br /&gt;
      response '422', 'missing title' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: '',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return current user properly&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['error']).to eq('title is missing')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for invalid suggestion given&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Sample Suggestion',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
          suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
          allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for deleting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    delete 'Delete suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion deletion&lt;br /&gt;
        response '204', 'suggestion deleted' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(204)&lt;br /&gt;
            expect(response.body).to be_empty&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for record not being destroyed&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the deletion process&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when suggestion is not found/doesn't exist&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students cannot delete suggestions&lt;br /&gt;
        response '403', 'students cannot delete suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for indexing/listing all suggestions&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    get 'list all suggestions' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
          suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful indexing&lt;br /&gt;
        response '200', 'suggestions listed' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
            expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for student to be forbidden from indexing suggestions&lt;br /&gt;
        response '403', 'students cannot index suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for rejecting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
    post 'Reject suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is instructor/ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion rejection&lt;br /&gt;
        response '200', 'suggestion rejected' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Rejected')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for suggestion already being approved&lt;br /&gt;
        response '422', 'suggestion already approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulate suggestion status as 'Approved'&lt;br /&gt;
            suggestion.update!(status: 'Approved')&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
            parsed_response = JSON.parse(response.body)&lt;br /&gt;
            expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when the suggestion not being found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
        response '403', 'students cannot reject suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for showing suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    get 'show suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when user is a instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
        response '200', 'suggestion details and comments returned' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when user is owner of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          # make sure user is set as owner of suggestion&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
          # test for student being able to view their own suggestion&lt;br /&gt;
          response '200', 'student can view their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              data = JSON.parse(response.body)&lt;br /&gt;
              expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
              expect(data['comments']).to be_an(Array)&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test case for when user is not owner of suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
          response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for updating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    patch 'update suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion update&lt;br /&gt;
        response '200', 'suggestion updated' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to be a student and setup current user&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is owner/a part of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test to make sure students can update their own suggestion(s)&lt;br /&gt;
          response '200', 'student can update their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is not owner to suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
          response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion being invalid in some form/missing&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
          allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Test Coverage===&lt;br /&gt;
[[File:TestCoverageSuggestionsController.png]]&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add detail to testing&lt;br /&gt;
* Record testing video&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Pranavi Sanganabhatla (psangan@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
* [# Testing video]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160765</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160765"/>
		<updated>2024-12-04T05:53:50Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Model associations */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
The controller is now fully commented, and this wiki page goes more in-depth about the code, design, and testing.&lt;br /&gt;
&lt;br /&gt;
Various other QoL changes are implemented, such as alphabetized hash parameters and collapsible source code views.&lt;br /&gt;
&lt;br /&gt;
The RSpec file is still under construction.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required: (skip steps 3 and 4 if automatic sign up is turned off)&lt;br /&gt;
## Mark the suggestion as approved&lt;br /&gt;
## Create a new topic from the suggestion&lt;br /&gt;
## Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
## Send the topic approved message&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Action &lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Method name&lt;br /&gt;
! Comment&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; | no change &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | add_comment&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve&lt;br /&gt;
| assimilated approve_suggestion&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | show&lt;br /&gt;
| assimilated student_view&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;5&amp;quot; | added &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create_topic_from_suggestion!&lt;br /&gt;
| chain of helper methods distilled into single call to create&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_student&lt;br /&gt;
| privilege check helper method, only TA and higher can pass&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_non_owner_student&lt;br /&gt;
| privilege check helper method, same as above, but student passes if he is the owner&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | destroy&lt;br /&gt;
| it should be possible to delete a suggestion (D in CRUD)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | send_notice_of_rejection&lt;br /&gt;
| complementary message to approval message&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | removed &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | action_allowed?&lt;br /&gt;
| different methods require different authorization checks, use dedicated helpers instead&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve_suggestion&lt;br /&gt;
| merged into approve&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | new&lt;br /&gt;
| view methods don't apply to JSON APIs&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_edit&lt;br /&gt;
| editing a suggestion is not allowed&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_view&lt;br /&gt;
| merged into show&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | submit&lt;br /&gt;
| call-this-or-that method&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | suggestion_params&lt;br /&gt;
| inlined into before_action declaration&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;6&amp;quot; | renamed &lt;br /&gt;
! Old&lt;br /&gt;
! New&lt;br /&gt;
!&lt;br /&gt;
|-&lt;br /&gt;
| list&lt;br /&gt;
| index&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| notification&lt;br /&gt;
| sign_team_up!&lt;br /&gt;
| method name and implementation didn't match&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion&lt;br /&gt;
| reject&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| send_email&lt;br /&gt;
| send_notice_of_approval&lt;br /&gt;
| method names should be specific&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion&lt;br /&gt;
| update&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code. Additionally, the suggestions controller now notifies the suggester if his suggestion has been rejected instead of just silently updating the state.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
This task specifically targeted the view files. Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
Even so, DRY was observed throughout the project by extracting common statements into helper functions, most notably the privilege check methods.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
===Tables===&lt;br /&gt;
&lt;br /&gt;
The following tables are newly added to the database schema. The question mark at the end of foreign_key indicates that this field is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:tables.png]]&lt;br /&gt;
&lt;br /&gt;
===Model associations===&lt;br /&gt;
&lt;br /&gt;
While simple at first glance, the suggestions controller interacts with many models in the system.&lt;br /&gt;
&lt;br /&gt;
The models in the first row of the diagram are the direct subject material of the controller as most API endpoints perform CRUD operations on only these two. The grayed out models in the second row are only ever read by the controller, they can be considered static throughout any call to the controller. The remaining four models in the last row and column of the diagram are handled by the suggestion approval process. The controller may perform any of the CRUD operations on these models.&lt;br /&gt;
&lt;br /&gt;
The arrows in the diagram point in the direction of ownership, that is, an arrow points from models A to B if A belongs to B. The dashed arrow labeled 'optional' in the legend means that the foreign key is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:ActiveRecord Associations.png]]&lt;br /&gt;
&lt;br /&gt;
===Controller and collaborators===&lt;br /&gt;
&lt;br /&gt;
The following diagram shows the controller, its API endpoints, its collaborators, and the methods accessed from the controller. The AuthorizationHelper module is imported into the controller class.&lt;br /&gt;
&lt;br /&gt;
[[File:classes.png]]&lt;br /&gt;
&lt;br /&gt;
==JSON API==&lt;br /&gt;
&lt;br /&gt;
The backend reimplementation project aims to convert Expertiza into a split frontend-backend service. To this end, the backend will respond with JSON only.&lt;br /&gt;
&lt;br /&gt;
===Request Format===&lt;br /&gt;
&lt;br /&gt;
In addition to the authorization headers, a combination of seven fields may be required for the specific API endpoint. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! rowspan=&amp;quot;2&amp;quot; | Parameter !! colspan=&amp;quot;8&amp;quot; | Included in API Endpoint !! rowspan=&amp;quot;2&amp;quot; | Type !! rowspan=&amp;quot;2&amp;quot; | Description&lt;br /&gt;
|-&lt;br /&gt;
! add_comment !! approve !! create !! destroy !! index !! reject !! show !! update&lt;br /&gt;
|-&lt;br /&gt;
! id&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓* || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || integer || Suggestion ID&lt;br /&gt;
|-&lt;br /&gt;
! anonymous&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || boolean || Whether to nullify user ID of suggester&lt;br /&gt;
|-&lt;br /&gt;
! assignment_id&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || integer || Assignment ID&lt;br /&gt;
|-&lt;br /&gt;
! auto_signup&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || boolean || Whether to sign up team when approved&lt;br /&gt;
|-&lt;br /&gt;
! comment&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || string || SuggestionComment content&lt;br /&gt;
|-&lt;br /&gt;
! description&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion description&lt;br /&gt;
|-&lt;br /&gt;
! title&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion title&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
* *: The id parameter specifies the Assignment ID instead&lt;br /&gt;
* ✓: Field required&lt;br /&gt;
* ✕: Field ignored&lt;br /&gt;
* ~: Field optional&lt;br /&gt;
&lt;br /&gt;
For example, the request data for the add_comment method would be { &amp;quot;id&amp;quot;: 1, &amp;quot;comment&amp;quot;: &amp;quot;This is a comment on one of the suggestions&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
===Status Codes===&lt;br /&gt;
&lt;br /&gt;
Depending on the parameters and database records, different HTTP status codes may be returned. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Code || Mnemonic || Condition || API Endpoints&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 2xx || success || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 200 || ok || Request successful || approve&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&amp;lt;br&amp;gt;(c&amp;lt;strong&amp;gt;RU&amp;lt;/strong&amp;gt;d)&lt;br /&gt;
|-&lt;br /&gt;
| 201 || created || Comment successfully created&amp;lt;br&amp;gt;SuggestionComment successfully created || add_comment&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;(&amp;lt;strong&amp;gt;C&amp;lt;/strong&amp;gt;rud)&lt;br /&gt;
|-&lt;br /&gt;
| 204 || no content || Suggestion successfully deleted || delete&amp;lt;br&amp;gt;(cru&amp;lt;strong&amp;gt;D&amp;lt;/strong&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 4xx || client error || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 403 || forbidden || Privilege check fails || approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 404 || not found || ActiveRecord::RecordNotFound exception || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 422 || unprocessable entity || ActiveRecord::RecordInvalid exception&amp;lt;br&amp;gt;ActiveRecord::RecordNotDestroyed exception&amp;lt;br&amp;gt;Suggestion already approved when trying to reject || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;update&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Response Format===&lt;br /&gt;
&lt;br /&gt;
Depending on the API endpoint of the request, a different entity may be returned. Note that only a success (2xx) will return an entity, a client error (4xx) will return an error message instead. The returned entities are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! API Endpoint || Rendered Model&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || SuggestionComment&lt;br /&gt;
|-&lt;br /&gt;
| approve || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| create || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| destroy || { }&lt;br /&gt;
|-&lt;br /&gt;
| index || Array of Suggestions&lt;br /&gt;
|-&lt;br /&gt;
| reject || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| show || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| update || Suggestion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
Each of the boxes with file names are expandable and contain the contents of the file. If applicable, a before-after view is provided.&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :title, :description, presence: true&lt;br /&gt;
  has_many :suggestion_comments&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :assignment&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
# https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  # Strong params inlined as global before_action&lt;br /&gt;
  before_action except: [:index] do&lt;br /&gt;
    params.require(:id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Comment on a suggestion.&lt;br /&gt;
  # A new SuggestionComment record is made.&lt;br /&gt;
  def add_comment&lt;br /&gt;
    params.require(:comment)&lt;br /&gt;
    Suggestion.find(params[:id])&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Approve a suggestion, even if it was previously rejected.&lt;br /&gt;
  def approve&lt;br /&gt;
    deny_student('Students cannot approve a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Only go through the approval process if the suggestion isn't already approved.&lt;br /&gt;
    unless @suggestion.status == 'Approved'&lt;br /&gt;
      # Since this process updates multiple records at once, wrap them in&lt;br /&gt;
      #   a transaction so that either all of them succeed, or none of them.&lt;br /&gt;
      transaction do&lt;br /&gt;
        # The approval process is:&lt;br /&gt;
        # 1. Mark the suggestion as approved&lt;br /&gt;
        # 2. Create a new topic from the suggestion&lt;br /&gt;
        # 3. Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
        # 4. Send the topic approved message&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        # If a suggestion is anonymous, the suggester is&lt;br /&gt;
        #  unknown and thus steps 3 and 4 can't be taken.&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up!&lt;br /&gt;
          send_notice_of_approval&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # A new Suggestion record is made.&lt;br /&gt;
  def create&lt;br /&gt;
    params.require(%i[anonymous assignment_id auto_signup comment description title])&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      # Anonymous suggestions are allowed by nulling user_id.&lt;br /&gt;
      user_id: params[:anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Delete a suggestion from the records.&lt;br /&gt;
  def destroy&lt;br /&gt;
    deny_student('Students cannot delete suggestions.')&lt;br /&gt;
    Suggestion.find(params[:id]).destroy!&lt;br /&gt;
    render json: {}, status: :no_content # 204&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a list of all Suggestion records associated with a particular assignment.&lt;br /&gt;
  def index&lt;br /&gt;
    deny_student('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
    render json: Suggestion.where(assignment_id: params[:id]), status: :ok # 200&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Reject a suggestion unless it was already approved.&lt;br /&gt;
  def reject&lt;br /&gt;
    deny_student('Students cannot reject a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Since the approval process makes changes to many records,&lt;br /&gt;
    #   rejecting a previously approved suggestion is not possible.&lt;br /&gt;
    if @suggestion.status == 'Approved'&lt;br /&gt;
      render json: { error: 'Suggestion has already been approved.' }, status: :unprocessable_entity # 422&lt;br /&gt;
    elsif @suggestion.status == 'Initialized'&lt;br /&gt;
      # The rejection process is:&lt;br /&gt;
      # 1. Mark the suggestion as rejected&lt;br /&gt;
      # 2. Send the topic rejected message&lt;br /&gt;
      @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      send_notice_of_rejection if @suggestion.user_id&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a single Suggestion record.&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only view their own suggestions.')&lt;br /&gt;
    render json: {&lt;br /&gt;
      comments: SuggestionComment.where(suggestion_id: params[:id]),&lt;br /&gt;
      suggestion: @suggestion&lt;br /&gt;
    }, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Change the details of a Suggestion.&lt;br /&gt;
  def update&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only edit their own suggestions.')&lt;br /&gt;
    # Only title, description, and signup preference can be changed.&lt;br /&gt;
    @suggestion.update!(params.permit(:title, :description, :auto_signup))&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    # Convert a suggestion into a fully fledged topic.&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1,&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_student(err_msg)&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_non_owner_student(err_msg)&lt;br /&gt;
    # If the student owns the resource, they are allowed to perform&lt;br /&gt;
    #   every action related to Suggestions and SuggestionComments.&lt;br /&gt;
    return if @suggestion.user_id == @current_user.id&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval&lt;br /&gt;
    # Email the suggester and CC the suggester's teammates that the suggestion was approved&lt;br /&gt;
    Mailer.send_topic_approved_email(&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      suggester: @suggester,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_rejection&lt;br /&gt;
    # Email the suggester that the suggestion was rejected&lt;br /&gt;
    Mailer.send_topic_rejected_email(&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been rejected&amp;quot;,&lt;br /&gt;
      suggester: User.find(@suggestion.user_id),&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    # Find the suggester's team which is signed up to the topic's assignment.&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    # Create the team if necessary.&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    # If the suggester's team has no assignment, then clear its&lt;br /&gt;
    #   waitlist and sign it up to the newly created topic.&lt;br /&gt;
    unless SignedUpTeam.exists?(team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: true).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    # Since the suggester's team is signed up to the topic, make it private&lt;br /&gt;
    #   to the suggester so no other teams will attempt to sign up to it.&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML.&amp;lt;br&amp;gt;The JSON API is described in [[#JSON API|JSON API]]&lt;br /&gt;
* Switched from check-avoid to fail-recover coding style to minimize number of if statements and thus indentation.&amp;lt;br&amp;gt;As a result, the methods become easier to understand, as the intended behavior is programmed all in one block with each error handled all the way at the end of the method&lt;br /&gt;
* Methods have proper error handling.&amp;lt;br&amp;gt;Each method was given a 'rescue' clause for any error that might occur, and most ActiveRecords methods were switched from the ones that return a boolean to the ones that throw an exception&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects and end in an exclamation point if the method modifies the database&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-segmented into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries.&amp;lt;br&amp;gt;Additionally, a complementary email method for rejections has been addded&lt;br /&gt;
* Where required, API entry points call dedicated privilege check methods&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, cc: defn[:cc], subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_topic_rejected_email(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;authorization_helper.rb (Trimmed)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (authorization_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_approved_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been approved.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you want to work on this topic, you can log into Expertiza and switch topics.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_rejected_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been rejected.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you believe this to be an error, please contact your instructor or one of the TAs.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The authorization helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
The mailer helper is accompanied by its template files for each type of message sent, in this case the topic approved and topic rejected messages. They also exist as plain text template files, since the original repo had both html and plaintext versions of the email templates. The plaintext versions of the template files are not shown as their content is the same, just stripped of the HTML tags.&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the following section:&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (show test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  get 'show suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when user is a instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
      response '200', 'suggestion details and comments returned' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
          expect(data['comments']).to be_an(Array)&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is owner of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        # make sure user is set as owner of suggestion&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
        # test for student being able to view their own suggestion&lt;br /&gt;
        response '200', 'student can view their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is not owner of suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
        response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for when suggestion doesn't exist&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (add comment test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
  post 'Add a comment to a suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
    parameter name: :comment, in: :body, schema: {&lt;br /&gt;
      type: :object,&lt;br /&gt;
      properties: {&lt;br /&gt;
        comment: { type: :string }&lt;br /&gt;
      },&lt;br /&gt;
      required: ['comment']&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # successful comment addition test&lt;br /&gt;
    response '201', 'comment_added' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for missing or empty comment&lt;br /&gt;
    response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        # Mock the params to simulate the request&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
        # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
      let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (approve test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
  post 'Approve suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # successful suggestion approval&lt;br /&gt;
      response '200', 'suggestion approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Approved')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable with a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable without a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when a suggestion is not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user as not having ta_privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for students not being able to approve suggestions&lt;br /&gt;
      response '403', 'students cannot approve suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (reject test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
  post 'Reject suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is instructor/ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion rejection&lt;br /&gt;
      response '200', 'suggestion rejected' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Rejected')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion already being approved&lt;br /&gt;
      response '422', 'suggestion already approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulate suggestion status as 'Approved'&lt;br /&gt;
          suggestion.update!(status: 'Approved')&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          parsed_response = JSON.parse(response.body)&lt;br /&gt;
          expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when the suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
      response '403', 'students cannot reject suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (edit/update test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  patch 'update suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion update&lt;br /&gt;
      response '200', 'suggestion updated' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to be a student and setup current user&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is owner/a part of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students can update their own suggestion(s)&lt;br /&gt;
        response '200', 'student can update their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is not owner to suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
        response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not being found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion being invalid in some form/missing&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
        allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (create test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  post 'Create suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
              description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
    # test for successful suggestion creation&lt;br /&gt;
    response '201', 'suggestion created successfully' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Suggestion title',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return properly for test&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
        expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
        expect(data['auto_signup']).to eq(true)&lt;br /&gt;
        expect(data['status']).to eq('Initialized')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for a missing parameter&lt;br /&gt;
    response '422', 'missing title' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: '',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return current user properly&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['error']).to eq('title is missing')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for invalid suggestion given&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Sample Suggestion',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
        allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  delete 'Delete suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion deletion&lt;br /&gt;
      response '204', 'suggestion deleted' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(204)&lt;br /&gt;
          expect(response.body).to be_empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for record not being destroyed&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the deletion process&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion is not found/doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test to make sure students cannot delete suggestions&lt;br /&gt;
      response '403', 'students cannot delete suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (index test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  get 'list all suggestions' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful indexing&lt;br /&gt;
      response '200', 'suggestions listed' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
         run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
          expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test case for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for student to be forbidden from indexing suggestions&lt;br /&gt;
      response '403', 'students cannot index suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Updated Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def login_user&lt;br /&gt;
  # Create a user using the factory&lt;br /&gt;
  user = create(:user)&lt;br /&gt;
&lt;br /&gt;
  # Make a POST request to login&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
&lt;br /&gt;
  # Parse the JSON response and extract the token&lt;br /&gt;
  json_response = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
  # Return the token from the response&lt;br /&gt;
  { token: json_response['token'], user: }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  # Login user, grab token, set user and current user&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    auth_data = login_user&lt;br /&gt;
    @token = auth_data[:token]&lt;br /&gt;
    @user = auth_data[:user]&lt;br /&gt;
    @current_user = @user&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # create default assignment and suggestion&lt;br /&gt;
  let(:assignment) { create(:assignment) }&lt;br /&gt;
  let(:suggestion) { create(:suggestion, assignment_id: assignment.id, user_id: @user.id) }&lt;br /&gt;
&lt;br /&gt;
  # Testing for add_comment method&lt;br /&gt;
  path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
    post 'Add a comment to a suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
      parameter name: :comment, in: :body, schema: {&lt;br /&gt;
        type: :object,&lt;br /&gt;
        properties: {&lt;br /&gt;
          comment: { type: :string }&lt;br /&gt;
        },&lt;br /&gt;
        required: ['comment']&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      # successful comment addition test&lt;br /&gt;
      response '201', 'comment_added' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for missing or empty comment&lt;br /&gt;
      response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Mock the params to simulate the request&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
          # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
        let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Tests for approving suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
    post 'Approve suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # successful suggestion approval&lt;br /&gt;
        response '200', 'suggestion approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Approved')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable with a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable without a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when a suggestion is not found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user as not having ta_privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for students not being able to approve suggestions&lt;br /&gt;
        response '403', 'students cannot approve suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for creating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    post 'Create suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
                description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion creation&lt;br /&gt;
      response '201', 'suggestion created successfully' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Suggestion title',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return properly for test&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
          expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
          expect(data['auto_signup']).to eq(true)&lt;br /&gt;
          expect(data['status']).to eq('Initialized')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for a missing parameter&lt;br /&gt;
      response '422', 'missing title' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: '',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return current user properly&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['error']).to eq('title is missing')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for invalid suggestion given&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Sample Suggestion',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
          suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
          allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for deleting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    delete 'Delete suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion deletion&lt;br /&gt;
        response '204', 'suggestion deleted' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(204)&lt;br /&gt;
            expect(response.body).to be_empty&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for record not being destroyed&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the deletion process&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when suggestion is not found/doesn't exist&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students cannot delete suggestions&lt;br /&gt;
        response '403', 'students cannot delete suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for indexing/listing all suggestions&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    get 'list all suggestions' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
          suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful indexing&lt;br /&gt;
        response '200', 'suggestions listed' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
            expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for student to be forbidden from indexing suggestions&lt;br /&gt;
        response '403', 'students cannot index suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for rejecting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
    post 'Reject suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is instructor/ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion rejection&lt;br /&gt;
        response '200', 'suggestion rejected' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Rejected')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for suggestion already being approved&lt;br /&gt;
        response '422', 'suggestion already approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulate suggestion status as 'Approved'&lt;br /&gt;
            suggestion.update!(status: 'Approved')&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
            parsed_response = JSON.parse(response.body)&lt;br /&gt;
            expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when the suggestion not being found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
        response '403', 'students cannot reject suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for showing suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    get 'show suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when user is a instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
        response '200', 'suggestion details and comments returned' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when user is owner of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          # make sure user is set as owner of suggestion&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
          # test for student being able to view their own suggestion&lt;br /&gt;
          response '200', 'student can view their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              data = JSON.parse(response.body)&lt;br /&gt;
              expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
              expect(data['comments']).to be_an(Array)&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test case for when user is not owner of suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
          response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for updating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    patch 'update suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion update&lt;br /&gt;
        response '200', 'suggestion updated' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to be a student and setup current user&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is owner/a part of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test to make sure students can update their own suggestion(s)&lt;br /&gt;
          response '200', 'student can update their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is not owner to suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
          response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion being invalid in some form/missing&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
          allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Test Coverage===&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add detail to testing&lt;br /&gt;
* Record testing video&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Pranavi Sanganabhatla (psangan@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
* [# Testing video]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160764</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160764"/>
		<updated>2024-12-04T05:53:00Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Model associations */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
The controller is now fully commented, and this wiki page goes more in-depth about the code, design, and testing.&lt;br /&gt;
&lt;br /&gt;
Various other QoL changes are implemented, such as alphabetized hash parameters and collapsible source code views.&lt;br /&gt;
&lt;br /&gt;
The RSpec file is still under construction.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required: (skip steps 3 and 4 if automatic sign up is turned off)&lt;br /&gt;
## Mark the suggestion as approved&lt;br /&gt;
## Create a new topic from the suggestion&lt;br /&gt;
## Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
## Send the topic approved message&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Action &lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Method name&lt;br /&gt;
! Comment&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; | no change &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | add_comment&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve&lt;br /&gt;
| assimilated approve_suggestion&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | show&lt;br /&gt;
| assimilated student_view&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;5&amp;quot; | added &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create_topic_from_suggestion!&lt;br /&gt;
| chain of helper methods distilled into single call to create&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_student&lt;br /&gt;
| privilege check helper method, only TA and higher can pass&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_non_owner_student&lt;br /&gt;
| privilege check helper method, same as above, but student passes if he is the owner&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | destroy&lt;br /&gt;
| it should be possible to delete a suggestion (D in CRUD)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | send_notice_of_rejection&lt;br /&gt;
| complementary message to approval message&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | removed &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | action_allowed?&lt;br /&gt;
| different methods require different authorization checks, use dedicated helpers instead&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve_suggestion&lt;br /&gt;
| merged into approve&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | new&lt;br /&gt;
| view methods don't apply to JSON APIs&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_edit&lt;br /&gt;
| editing a suggestion is not allowed&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_view&lt;br /&gt;
| merged into show&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | submit&lt;br /&gt;
| call-this-or-that method&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | suggestion_params&lt;br /&gt;
| inlined into before_action declaration&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;6&amp;quot; | renamed &lt;br /&gt;
! Old&lt;br /&gt;
! New&lt;br /&gt;
!&lt;br /&gt;
|-&lt;br /&gt;
| list&lt;br /&gt;
| index&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| notification&lt;br /&gt;
| sign_team_up!&lt;br /&gt;
| method name and implementation didn't match&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion&lt;br /&gt;
| reject&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| send_email&lt;br /&gt;
| send_notice_of_approval&lt;br /&gt;
| method names should be specific&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion&lt;br /&gt;
| update&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code. Additionally, the suggestions controller now notifies the suggester if his suggestion has been rejected instead of just silently updating the state.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
This task specifically targeted the view files. Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
Even so, DRY was observed throughout the project by extracting common statements into helper functions, most notably the privilege check methods.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
===Tables===&lt;br /&gt;
&lt;br /&gt;
The following tables are newly added to the database schema. The question mark at the end of foreign_key indicates that this field is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:tables.png]]&lt;br /&gt;
&lt;br /&gt;
===Model associations===&lt;br /&gt;
&lt;br /&gt;
While simple at first glance, the suggestions controller interacts with many models in the system.&lt;br /&gt;
&lt;br /&gt;
The models in the first row of the diagram are the direct subject material of the controller as most API endpoints perform CRUD operations on only these two. The grayed out models in the second row are only ever read by the controller, they can be considered static throughout any call to the controller. The remaining four models in the last row and column of the diagram are handled by the suggestion approval process. The controller may perform any of the CRUD operations on these models.&lt;br /&gt;
&lt;br /&gt;
The arrows in the diagram point in the direction of ownership, that is, an arrow points from models A to B if A belongs to B. The dashed arrow labeled 'optional' in the legend means that the foreign key is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:TestCoverageSuggestionsController.png]]&lt;br /&gt;
&lt;br /&gt;
===Controller and collaborators===&lt;br /&gt;
&lt;br /&gt;
The following diagram shows the controller, its API endpoints, its collaborators, and the methods accessed from the controller. The AuthorizationHelper module is imported into the controller class.&lt;br /&gt;
&lt;br /&gt;
[[File:classes.png]]&lt;br /&gt;
&lt;br /&gt;
==JSON API==&lt;br /&gt;
&lt;br /&gt;
The backend reimplementation project aims to convert Expertiza into a split frontend-backend service. To this end, the backend will respond with JSON only.&lt;br /&gt;
&lt;br /&gt;
===Request Format===&lt;br /&gt;
&lt;br /&gt;
In addition to the authorization headers, a combination of seven fields may be required for the specific API endpoint. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! rowspan=&amp;quot;2&amp;quot; | Parameter !! colspan=&amp;quot;8&amp;quot; | Included in API Endpoint !! rowspan=&amp;quot;2&amp;quot; | Type !! rowspan=&amp;quot;2&amp;quot; | Description&lt;br /&gt;
|-&lt;br /&gt;
! add_comment !! approve !! create !! destroy !! index !! reject !! show !! update&lt;br /&gt;
|-&lt;br /&gt;
! id&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓* || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || integer || Suggestion ID&lt;br /&gt;
|-&lt;br /&gt;
! anonymous&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || boolean || Whether to nullify user ID of suggester&lt;br /&gt;
|-&lt;br /&gt;
! assignment_id&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || integer || Assignment ID&lt;br /&gt;
|-&lt;br /&gt;
! auto_signup&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || boolean || Whether to sign up team when approved&lt;br /&gt;
|-&lt;br /&gt;
! comment&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || string || SuggestionComment content&lt;br /&gt;
|-&lt;br /&gt;
! description&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion description&lt;br /&gt;
|-&lt;br /&gt;
! title&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion title&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
* *: The id parameter specifies the Assignment ID instead&lt;br /&gt;
* ✓: Field required&lt;br /&gt;
* ✕: Field ignored&lt;br /&gt;
* ~: Field optional&lt;br /&gt;
&lt;br /&gt;
For example, the request data for the add_comment method would be { &amp;quot;id&amp;quot;: 1, &amp;quot;comment&amp;quot;: &amp;quot;This is a comment on one of the suggestions&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
===Status Codes===&lt;br /&gt;
&lt;br /&gt;
Depending on the parameters and database records, different HTTP status codes may be returned. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Code || Mnemonic || Condition || API Endpoints&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 2xx || success || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 200 || ok || Request successful || approve&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&amp;lt;br&amp;gt;(c&amp;lt;strong&amp;gt;RU&amp;lt;/strong&amp;gt;d)&lt;br /&gt;
|-&lt;br /&gt;
| 201 || created || Comment successfully created&amp;lt;br&amp;gt;SuggestionComment successfully created || add_comment&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;(&amp;lt;strong&amp;gt;C&amp;lt;/strong&amp;gt;rud)&lt;br /&gt;
|-&lt;br /&gt;
| 204 || no content || Suggestion successfully deleted || delete&amp;lt;br&amp;gt;(cru&amp;lt;strong&amp;gt;D&amp;lt;/strong&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 4xx || client error || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 403 || forbidden || Privilege check fails || approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 404 || not found || ActiveRecord::RecordNotFound exception || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 422 || unprocessable entity || ActiveRecord::RecordInvalid exception&amp;lt;br&amp;gt;ActiveRecord::RecordNotDestroyed exception&amp;lt;br&amp;gt;Suggestion already approved when trying to reject || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;update&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Response Format===&lt;br /&gt;
&lt;br /&gt;
Depending on the API endpoint of the request, a different entity may be returned. Note that only a success (2xx) will return an entity, a client error (4xx) will return an error message instead. The returned entities are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! API Endpoint || Rendered Model&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || SuggestionComment&lt;br /&gt;
|-&lt;br /&gt;
| approve || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| create || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| destroy || { }&lt;br /&gt;
|-&lt;br /&gt;
| index || Array of Suggestions&lt;br /&gt;
|-&lt;br /&gt;
| reject || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| show || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| update || Suggestion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
Each of the boxes with file names are expandable and contain the contents of the file. If applicable, a before-after view is provided.&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :title, :description, presence: true&lt;br /&gt;
  has_many :suggestion_comments&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :assignment&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
# https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  # Strong params inlined as global before_action&lt;br /&gt;
  before_action except: [:index] do&lt;br /&gt;
    params.require(:id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Comment on a suggestion.&lt;br /&gt;
  # A new SuggestionComment record is made.&lt;br /&gt;
  def add_comment&lt;br /&gt;
    params.require(:comment)&lt;br /&gt;
    Suggestion.find(params[:id])&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Approve a suggestion, even if it was previously rejected.&lt;br /&gt;
  def approve&lt;br /&gt;
    deny_student('Students cannot approve a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Only go through the approval process if the suggestion isn't already approved.&lt;br /&gt;
    unless @suggestion.status == 'Approved'&lt;br /&gt;
      # Since this process updates multiple records at once, wrap them in&lt;br /&gt;
      #   a transaction so that either all of them succeed, or none of them.&lt;br /&gt;
      transaction do&lt;br /&gt;
        # The approval process is:&lt;br /&gt;
        # 1. Mark the suggestion as approved&lt;br /&gt;
        # 2. Create a new topic from the suggestion&lt;br /&gt;
        # 3. Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
        # 4. Send the topic approved message&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        # If a suggestion is anonymous, the suggester is&lt;br /&gt;
        #  unknown and thus steps 3 and 4 can't be taken.&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up!&lt;br /&gt;
          send_notice_of_approval&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # A new Suggestion record is made.&lt;br /&gt;
  def create&lt;br /&gt;
    params.require(%i[anonymous assignment_id auto_signup comment description title])&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      # Anonymous suggestions are allowed by nulling user_id.&lt;br /&gt;
      user_id: params[:anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Delete a suggestion from the records.&lt;br /&gt;
  def destroy&lt;br /&gt;
    deny_student('Students cannot delete suggestions.')&lt;br /&gt;
    Suggestion.find(params[:id]).destroy!&lt;br /&gt;
    render json: {}, status: :no_content # 204&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a list of all Suggestion records associated with a particular assignment.&lt;br /&gt;
  def index&lt;br /&gt;
    deny_student('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
    render json: Suggestion.where(assignment_id: params[:id]), status: :ok # 200&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Reject a suggestion unless it was already approved.&lt;br /&gt;
  def reject&lt;br /&gt;
    deny_student('Students cannot reject a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Since the approval process makes changes to many records,&lt;br /&gt;
    #   rejecting a previously approved suggestion is not possible.&lt;br /&gt;
    if @suggestion.status == 'Approved'&lt;br /&gt;
      render json: { error: 'Suggestion has already been approved.' }, status: :unprocessable_entity # 422&lt;br /&gt;
    elsif @suggestion.status == 'Initialized'&lt;br /&gt;
      # The rejection process is:&lt;br /&gt;
      # 1. Mark the suggestion as rejected&lt;br /&gt;
      # 2. Send the topic rejected message&lt;br /&gt;
      @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      send_notice_of_rejection if @suggestion.user_id&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a single Suggestion record.&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only view their own suggestions.')&lt;br /&gt;
    render json: {&lt;br /&gt;
      comments: SuggestionComment.where(suggestion_id: params[:id]),&lt;br /&gt;
      suggestion: @suggestion&lt;br /&gt;
    }, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Change the details of a Suggestion.&lt;br /&gt;
  def update&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only edit their own suggestions.')&lt;br /&gt;
    # Only title, description, and signup preference can be changed.&lt;br /&gt;
    @suggestion.update!(params.permit(:title, :description, :auto_signup))&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    # Convert a suggestion into a fully fledged topic.&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1,&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_student(err_msg)&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_non_owner_student(err_msg)&lt;br /&gt;
    # If the student owns the resource, they are allowed to perform&lt;br /&gt;
    #   every action related to Suggestions and SuggestionComments.&lt;br /&gt;
    return if @suggestion.user_id == @current_user.id&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval&lt;br /&gt;
    # Email the suggester and CC the suggester's teammates that the suggestion was approved&lt;br /&gt;
    Mailer.send_topic_approved_email(&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      suggester: @suggester,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_rejection&lt;br /&gt;
    # Email the suggester that the suggestion was rejected&lt;br /&gt;
    Mailer.send_topic_rejected_email(&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been rejected&amp;quot;,&lt;br /&gt;
      suggester: User.find(@suggestion.user_id),&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    # Find the suggester's team which is signed up to the topic's assignment.&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    # Create the team if necessary.&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    # If the suggester's team has no assignment, then clear its&lt;br /&gt;
    #   waitlist and sign it up to the newly created topic.&lt;br /&gt;
    unless SignedUpTeam.exists?(team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: true).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    # Since the suggester's team is signed up to the topic, make it private&lt;br /&gt;
    #   to the suggester so no other teams will attempt to sign up to it.&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML.&amp;lt;br&amp;gt;The JSON API is described in [[#JSON API|JSON API]]&lt;br /&gt;
* Switched from check-avoid to fail-recover coding style to minimize number of if statements and thus indentation.&amp;lt;br&amp;gt;As a result, the methods become easier to understand, as the intended behavior is programmed all in one block with each error handled all the way at the end of the method&lt;br /&gt;
* Methods have proper error handling.&amp;lt;br&amp;gt;Each method was given a 'rescue' clause for any error that might occur, and most ActiveRecords methods were switched from the ones that return a boolean to the ones that throw an exception&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects and end in an exclamation point if the method modifies the database&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-segmented into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries.&amp;lt;br&amp;gt;Additionally, a complementary email method for rejections has been addded&lt;br /&gt;
* Where required, API entry points call dedicated privilege check methods&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, cc: defn[:cc], subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_topic_rejected_email(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;authorization_helper.rb (Trimmed)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (authorization_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_approved_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been approved.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you want to work on this topic, you can log into Expertiza and switch topics.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_rejected_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been rejected.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you believe this to be an error, please contact your instructor or one of the TAs.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The authorization helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
The mailer helper is accompanied by its template files for each type of message sent, in this case the topic approved and topic rejected messages. They also exist as plain text template files, since the original repo had both html and plaintext versions of the email templates. The plaintext versions of the template files are not shown as their content is the same, just stripped of the HTML tags.&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the following section:&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (show test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  get 'show suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when user is a instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
      response '200', 'suggestion details and comments returned' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
          expect(data['comments']).to be_an(Array)&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is owner of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        # make sure user is set as owner of suggestion&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
        # test for student being able to view their own suggestion&lt;br /&gt;
        response '200', 'student can view their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is not owner of suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
        response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for when suggestion doesn't exist&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (add comment test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
  post 'Add a comment to a suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
    parameter name: :comment, in: :body, schema: {&lt;br /&gt;
      type: :object,&lt;br /&gt;
      properties: {&lt;br /&gt;
        comment: { type: :string }&lt;br /&gt;
      },&lt;br /&gt;
      required: ['comment']&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # successful comment addition test&lt;br /&gt;
    response '201', 'comment_added' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for missing or empty comment&lt;br /&gt;
    response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        # Mock the params to simulate the request&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
        # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
      let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (approve test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
  post 'Approve suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # successful suggestion approval&lt;br /&gt;
      response '200', 'suggestion approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Approved')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable with a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable without a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when a suggestion is not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user as not having ta_privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for students not being able to approve suggestions&lt;br /&gt;
      response '403', 'students cannot approve suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (reject test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
  post 'Reject suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is instructor/ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion rejection&lt;br /&gt;
      response '200', 'suggestion rejected' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Rejected')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion already being approved&lt;br /&gt;
      response '422', 'suggestion already approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulate suggestion status as 'Approved'&lt;br /&gt;
          suggestion.update!(status: 'Approved')&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          parsed_response = JSON.parse(response.body)&lt;br /&gt;
          expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when the suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
      response '403', 'students cannot reject suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (edit/update test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  patch 'update suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion update&lt;br /&gt;
      response '200', 'suggestion updated' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to be a student and setup current user&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is owner/a part of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students can update their own suggestion(s)&lt;br /&gt;
        response '200', 'student can update their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is not owner to suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
        response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not being found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion being invalid in some form/missing&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
        allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (create test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  post 'Create suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
              description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
    # test for successful suggestion creation&lt;br /&gt;
    response '201', 'suggestion created successfully' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Suggestion title',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return properly for test&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
        expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
        expect(data['auto_signup']).to eq(true)&lt;br /&gt;
        expect(data['status']).to eq('Initialized')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for a missing parameter&lt;br /&gt;
    response '422', 'missing title' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: '',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return current user properly&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['error']).to eq('title is missing')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for invalid suggestion given&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Sample Suggestion',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
        allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  delete 'Delete suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion deletion&lt;br /&gt;
      response '204', 'suggestion deleted' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(204)&lt;br /&gt;
          expect(response.body).to be_empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for record not being destroyed&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the deletion process&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion is not found/doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test to make sure students cannot delete suggestions&lt;br /&gt;
      response '403', 'students cannot delete suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (index test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  get 'list all suggestions' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful indexing&lt;br /&gt;
      response '200', 'suggestions listed' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
         run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
          expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test case for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for student to be forbidden from indexing suggestions&lt;br /&gt;
      response '403', 'students cannot index suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Updated Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def login_user&lt;br /&gt;
  # Create a user using the factory&lt;br /&gt;
  user = create(:user)&lt;br /&gt;
&lt;br /&gt;
  # Make a POST request to login&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
&lt;br /&gt;
  # Parse the JSON response and extract the token&lt;br /&gt;
  json_response = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
  # Return the token from the response&lt;br /&gt;
  { token: json_response['token'], user: }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  # Login user, grab token, set user and current user&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    auth_data = login_user&lt;br /&gt;
    @token = auth_data[:token]&lt;br /&gt;
    @user = auth_data[:user]&lt;br /&gt;
    @current_user = @user&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # create default assignment and suggestion&lt;br /&gt;
  let(:assignment) { create(:assignment) }&lt;br /&gt;
  let(:suggestion) { create(:suggestion, assignment_id: assignment.id, user_id: @user.id) }&lt;br /&gt;
&lt;br /&gt;
  # Testing for add_comment method&lt;br /&gt;
  path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
    post 'Add a comment to a suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
      parameter name: :comment, in: :body, schema: {&lt;br /&gt;
        type: :object,&lt;br /&gt;
        properties: {&lt;br /&gt;
          comment: { type: :string }&lt;br /&gt;
        },&lt;br /&gt;
        required: ['comment']&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      # successful comment addition test&lt;br /&gt;
      response '201', 'comment_added' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for missing or empty comment&lt;br /&gt;
      response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Mock the params to simulate the request&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
          # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
        let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Tests for approving suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
    post 'Approve suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # successful suggestion approval&lt;br /&gt;
        response '200', 'suggestion approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Approved')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable with a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable without a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when a suggestion is not found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user as not having ta_privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for students not being able to approve suggestions&lt;br /&gt;
        response '403', 'students cannot approve suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for creating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    post 'Create suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
                description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion creation&lt;br /&gt;
      response '201', 'suggestion created successfully' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Suggestion title',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return properly for test&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
          expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
          expect(data['auto_signup']).to eq(true)&lt;br /&gt;
          expect(data['status']).to eq('Initialized')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for a missing parameter&lt;br /&gt;
      response '422', 'missing title' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: '',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return current user properly&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['error']).to eq('title is missing')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for invalid suggestion given&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Sample Suggestion',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
          suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
          allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for deleting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    delete 'Delete suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion deletion&lt;br /&gt;
        response '204', 'suggestion deleted' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(204)&lt;br /&gt;
            expect(response.body).to be_empty&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for record not being destroyed&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the deletion process&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when suggestion is not found/doesn't exist&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students cannot delete suggestions&lt;br /&gt;
        response '403', 'students cannot delete suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for indexing/listing all suggestions&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    get 'list all suggestions' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
          suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful indexing&lt;br /&gt;
        response '200', 'suggestions listed' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
            expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for student to be forbidden from indexing suggestions&lt;br /&gt;
        response '403', 'students cannot index suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for rejecting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
    post 'Reject suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is instructor/ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion rejection&lt;br /&gt;
        response '200', 'suggestion rejected' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Rejected')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for suggestion already being approved&lt;br /&gt;
        response '422', 'suggestion already approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulate suggestion status as 'Approved'&lt;br /&gt;
            suggestion.update!(status: 'Approved')&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
            parsed_response = JSON.parse(response.body)&lt;br /&gt;
            expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when the suggestion not being found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
        response '403', 'students cannot reject suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for showing suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    get 'show suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when user is a instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
        response '200', 'suggestion details and comments returned' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when user is owner of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          # make sure user is set as owner of suggestion&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
          # test for student being able to view their own suggestion&lt;br /&gt;
          response '200', 'student can view their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              data = JSON.parse(response.body)&lt;br /&gt;
              expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
              expect(data['comments']).to be_an(Array)&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test case for when user is not owner of suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
          response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for updating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    patch 'update suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion update&lt;br /&gt;
        response '200', 'suggestion updated' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to be a student and setup current user&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is owner/a part of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test to make sure students can update their own suggestion(s)&lt;br /&gt;
          response '200', 'student can update their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is not owner to suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
          response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion being invalid in some form/missing&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
          allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Test Coverage===&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add detail to testing&lt;br /&gt;
* Record testing video&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Pranavi Sanganabhatla (psangan@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
* [# Testing video]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=File:TestCoverageSuggestionsController.png&amp;diff=160763</id>
		<title>File:TestCoverageSuggestionsController.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=File:TestCoverageSuggestionsController.png&amp;diff=160763"/>
		<updated>2024-12-04T05:51:59Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160762</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160762"/>
		<updated>2024-12-04T05:48:57Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Test coverage */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
The controller is now fully commented, and this wiki page goes more in-depth about the code, design, and testing.&lt;br /&gt;
&lt;br /&gt;
Various other QoL changes are implemented, such as alphabetized hash parameters and collapsible source code views.&lt;br /&gt;
&lt;br /&gt;
The RSpec file is still under construction.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required: (skip steps 3 and 4 if automatic sign up is turned off)&lt;br /&gt;
## Mark the suggestion as approved&lt;br /&gt;
## Create a new topic from the suggestion&lt;br /&gt;
## Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
## Send the topic approved message&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Action &lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Method name&lt;br /&gt;
! Comment&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; | no change &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | add_comment&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve&lt;br /&gt;
| assimilated approve_suggestion&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | show&lt;br /&gt;
| assimilated student_view&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;5&amp;quot; | added &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create_topic_from_suggestion!&lt;br /&gt;
| chain of helper methods distilled into single call to create&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_student&lt;br /&gt;
| privilege check helper method, only TA and higher can pass&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_non_owner_student&lt;br /&gt;
| privilege check helper method, same as above, but student passes if he is the owner&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | destroy&lt;br /&gt;
| it should be possible to delete a suggestion (D in CRUD)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | send_notice_of_rejection&lt;br /&gt;
| complementary message to approval message&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | removed &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | action_allowed?&lt;br /&gt;
| different methods require different authorization checks, use dedicated helpers instead&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve_suggestion&lt;br /&gt;
| merged into approve&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | new&lt;br /&gt;
| view methods don't apply to JSON APIs&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_edit&lt;br /&gt;
| editing a suggestion is not allowed&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_view&lt;br /&gt;
| merged into show&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | submit&lt;br /&gt;
| call-this-or-that method&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | suggestion_params&lt;br /&gt;
| inlined into before_action declaration&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;6&amp;quot; | renamed &lt;br /&gt;
! Old&lt;br /&gt;
! New&lt;br /&gt;
!&lt;br /&gt;
|-&lt;br /&gt;
| list&lt;br /&gt;
| index&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| notification&lt;br /&gt;
| sign_team_up!&lt;br /&gt;
| method name and implementation didn't match&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion&lt;br /&gt;
| reject&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| send_email&lt;br /&gt;
| send_notice_of_approval&lt;br /&gt;
| method names should be specific&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion&lt;br /&gt;
| update&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code. Additionally, the suggestions controller now notifies the suggester if his suggestion has been rejected instead of just silently updating the state.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
This task specifically targeted the view files. Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
Even so, DRY was observed throughout the project by extracting common statements into helper functions, most notably the privilege check methods.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
===Tables===&lt;br /&gt;
&lt;br /&gt;
The following tables are newly added to the database schema. The question mark at the end of foreign_key indicates that this field is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:tables.png]]&lt;br /&gt;
&lt;br /&gt;
===Model associations===&lt;br /&gt;
&lt;br /&gt;
While simple at first glance, the suggestions controller interacts with many models in the system.&lt;br /&gt;
&lt;br /&gt;
The models in the first row of the diagram are the direct subject material of the controller as most API endpoints perform CRUD operations on only these two. The grayed out models in the second row are only ever read by the controller, they can be considered static throughout any call to the controller. The remaining four models in the last row and column of the diagram are handled by the suggestion approval process. The controller may perform any of the CRUD operations on these models.&lt;br /&gt;
&lt;br /&gt;
The arrows in the diagram point in the direction of ownership, that is, an arrow points from models A to B if A belongs to B. The dashed arrow labeled 'optional' in the legend means that the foreign key is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:ActiveRecord Associations.png]]&lt;br /&gt;
&lt;br /&gt;
===Controller and collaborators===&lt;br /&gt;
&lt;br /&gt;
The following diagram shows the controller, its API endpoints, its collaborators, and the methods accessed from the controller. The AuthorizationHelper module is imported into the controller class.&lt;br /&gt;
&lt;br /&gt;
[[File:classes.png]]&lt;br /&gt;
&lt;br /&gt;
==JSON API==&lt;br /&gt;
&lt;br /&gt;
The backend reimplementation project aims to convert Expertiza into a split frontend-backend service. To this end, the backend will respond with JSON only.&lt;br /&gt;
&lt;br /&gt;
===Request Format===&lt;br /&gt;
&lt;br /&gt;
In addition to the authorization headers, a combination of seven fields may be required for the specific API endpoint. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! rowspan=&amp;quot;2&amp;quot; | Parameter !! colspan=&amp;quot;8&amp;quot; | Included in API Endpoint !! rowspan=&amp;quot;2&amp;quot; | Type !! rowspan=&amp;quot;2&amp;quot; | Description&lt;br /&gt;
|-&lt;br /&gt;
! add_comment !! approve !! create !! destroy !! index !! reject !! show !! update&lt;br /&gt;
|-&lt;br /&gt;
! id&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓* || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || integer || Suggestion ID&lt;br /&gt;
|-&lt;br /&gt;
! anonymous&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || boolean || Whether to nullify user ID of suggester&lt;br /&gt;
|-&lt;br /&gt;
! assignment_id&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || integer || Assignment ID&lt;br /&gt;
|-&lt;br /&gt;
! auto_signup&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || boolean || Whether to sign up team when approved&lt;br /&gt;
|-&lt;br /&gt;
! comment&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || string || SuggestionComment content&lt;br /&gt;
|-&lt;br /&gt;
! description&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion description&lt;br /&gt;
|-&lt;br /&gt;
! title&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion title&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
* *: The id parameter specifies the Assignment ID instead&lt;br /&gt;
* ✓: Field required&lt;br /&gt;
* ✕: Field ignored&lt;br /&gt;
* ~: Field optional&lt;br /&gt;
&lt;br /&gt;
For example, the request data for the add_comment method would be { &amp;quot;id&amp;quot;: 1, &amp;quot;comment&amp;quot;: &amp;quot;This is a comment on one of the suggestions&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
===Status Codes===&lt;br /&gt;
&lt;br /&gt;
Depending on the parameters and database records, different HTTP status codes may be returned. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Code || Mnemonic || Condition || API Endpoints&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 2xx || success || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 200 || ok || Request successful || approve&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&amp;lt;br&amp;gt;(c&amp;lt;strong&amp;gt;RU&amp;lt;/strong&amp;gt;d)&lt;br /&gt;
|-&lt;br /&gt;
| 201 || created || Comment successfully created&amp;lt;br&amp;gt;SuggestionComment successfully created || add_comment&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;(&amp;lt;strong&amp;gt;C&amp;lt;/strong&amp;gt;rud)&lt;br /&gt;
|-&lt;br /&gt;
| 204 || no content || Suggestion successfully deleted || delete&amp;lt;br&amp;gt;(cru&amp;lt;strong&amp;gt;D&amp;lt;/strong&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 4xx || client error || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 403 || forbidden || Privilege check fails || approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 404 || not found || ActiveRecord::RecordNotFound exception || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 422 || unprocessable entity || ActiveRecord::RecordInvalid exception&amp;lt;br&amp;gt;ActiveRecord::RecordNotDestroyed exception&amp;lt;br&amp;gt;Suggestion already approved when trying to reject || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;update&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Response Format===&lt;br /&gt;
&lt;br /&gt;
Depending on the API endpoint of the request, a different entity may be returned. Note that only a success (2xx) will return an entity, a client error (4xx) will return an error message instead. The returned entities are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! API Endpoint || Rendered Model&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || SuggestionComment&lt;br /&gt;
|-&lt;br /&gt;
| approve || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| create || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| destroy || { }&lt;br /&gt;
|-&lt;br /&gt;
| index || Array of Suggestions&lt;br /&gt;
|-&lt;br /&gt;
| reject || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| show || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| update || Suggestion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
Each of the boxes with file names are expandable and contain the contents of the file. If applicable, a before-after view is provided.&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :title, :description, presence: true&lt;br /&gt;
  has_many :suggestion_comments&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :assignment&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
# https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  # Strong params inlined as global before_action&lt;br /&gt;
  before_action except: [:index] do&lt;br /&gt;
    params.require(:id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Comment on a suggestion.&lt;br /&gt;
  # A new SuggestionComment record is made.&lt;br /&gt;
  def add_comment&lt;br /&gt;
    params.require(:comment)&lt;br /&gt;
    Suggestion.find(params[:id])&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Approve a suggestion, even if it was previously rejected.&lt;br /&gt;
  def approve&lt;br /&gt;
    deny_student('Students cannot approve a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Only go through the approval process if the suggestion isn't already approved.&lt;br /&gt;
    unless @suggestion.status == 'Approved'&lt;br /&gt;
      # Since this process updates multiple records at once, wrap them in&lt;br /&gt;
      #   a transaction so that either all of them succeed, or none of them.&lt;br /&gt;
      transaction do&lt;br /&gt;
        # The approval process is:&lt;br /&gt;
        # 1. Mark the suggestion as approved&lt;br /&gt;
        # 2. Create a new topic from the suggestion&lt;br /&gt;
        # 3. Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
        # 4. Send the topic approved message&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        # If a suggestion is anonymous, the suggester is&lt;br /&gt;
        #  unknown and thus steps 3 and 4 can't be taken.&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up!&lt;br /&gt;
          send_notice_of_approval&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # A new Suggestion record is made.&lt;br /&gt;
  def create&lt;br /&gt;
    params.require(%i[anonymous assignment_id auto_signup comment description title])&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      # Anonymous suggestions are allowed by nulling user_id.&lt;br /&gt;
      user_id: params[:anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Delete a suggestion from the records.&lt;br /&gt;
  def destroy&lt;br /&gt;
    deny_student('Students cannot delete suggestions.')&lt;br /&gt;
    Suggestion.find(params[:id]).destroy!&lt;br /&gt;
    render json: {}, status: :no_content # 204&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a list of all Suggestion records associated with a particular assignment.&lt;br /&gt;
  def index&lt;br /&gt;
    deny_student('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
    render json: Suggestion.where(assignment_id: params[:id]), status: :ok # 200&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Reject a suggestion unless it was already approved.&lt;br /&gt;
  def reject&lt;br /&gt;
    deny_student('Students cannot reject a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Since the approval process makes changes to many records,&lt;br /&gt;
    #   rejecting a previously approved suggestion is not possible.&lt;br /&gt;
    if @suggestion.status == 'Approved'&lt;br /&gt;
      render json: { error: 'Suggestion has already been approved.' }, status: :unprocessable_entity # 422&lt;br /&gt;
    elsif @suggestion.status == 'Initialized'&lt;br /&gt;
      # The rejection process is:&lt;br /&gt;
      # 1. Mark the suggestion as rejected&lt;br /&gt;
      # 2. Send the topic rejected message&lt;br /&gt;
      @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      send_notice_of_rejection if @suggestion.user_id&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a single Suggestion record.&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only view their own suggestions.')&lt;br /&gt;
    render json: {&lt;br /&gt;
      comments: SuggestionComment.where(suggestion_id: params[:id]),&lt;br /&gt;
      suggestion: @suggestion&lt;br /&gt;
    }, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Change the details of a Suggestion.&lt;br /&gt;
  def update&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only edit their own suggestions.')&lt;br /&gt;
    # Only title, description, and signup preference can be changed.&lt;br /&gt;
    @suggestion.update!(params.permit(:title, :description, :auto_signup))&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    # Convert a suggestion into a fully fledged topic.&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1,&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_student(err_msg)&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_non_owner_student(err_msg)&lt;br /&gt;
    # If the student owns the resource, they are allowed to perform&lt;br /&gt;
    #   every action related to Suggestions and SuggestionComments.&lt;br /&gt;
    return if @suggestion.user_id == @current_user.id&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval&lt;br /&gt;
    # Email the suggester and CC the suggester's teammates that the suggestion was approved&lt;br /&gt;
    Mailer.send_topic_approved_email(&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      suggester: @suggester,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_rejection&lt;br /&gt;
    # Email the suggester that the suggestion was rejected&lt;br /&gt;
    Mailer.send_topic_rejected_email(&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been rejected&amp;quot;,&lt;br /&gt;
      suggester: User.find(@suggestion.user_id),&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    # Find the suggester's team which is signed up to the topic's assignment.&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    # Create the team if necessary.&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    # If the suggester's team has no assignment, then clear its&lt;br /&gt;
    #   waitlist and sign it up to the newly created topic.&lt;br /&gt;
    unless SignedUpTeam.exists?(team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: true).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    # Since the suggester's team is signed up to the topic, make it private&lt;br /&gt;
    #   to the suggester so no other teams will attempt to sign up to it.&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML.&amp;lt;br&amp;gt;The JSON API is described in [[#JSON API|JSON API]]&lt;br /&gt;
* Switched from check-avoid to fail-recover coding style to minimize number of if statements and thus indentation.&amp;lt;br&amp;gt;As a result, the methods become easier to understand, as the intended behavior is programmed all in one block with each error handled all the way at the end of the method&lt;br /&gt;
* Methods have proper error handling.&amp;lt;br&amp;gt;Each method was given a 'rescue' clause for any error that might occur, and most ActiveRecords methods were switched from the ones that return a boolean to the ones that throw an exception&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects and end in an exclamation point if the method modifies the database&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-segmented into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries.&amp;lt;br&amp;gt;Additionally, a complementary email method for rejections has been addded&lt;br /&gt;
* Where required, API entry points call dedicated privilege check methods&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, cc: defn[:cc], subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_topic_rejected_email(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;authorization_helper.rb (Trimmed)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (authorization_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_approved_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been approved.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you want to work on this topic, you can log into Expertiza and switch topics.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_rejected_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been rejected.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you believe this to be an error, please contact your instructor or one of the TAs.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The authorization helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
The mailer helper is accompanied by its template files for each type of message sent, in this case the topic approved and topic rejected messages. They also exist as plain text template files, since the original repo had both html and plaintext versions of the email templates. The plaintext versions of the template files are not shown as their content is the same, just stripped of the HTML tags.&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the following section:&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (show test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  get 'show suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when user is a instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
      response '200', 'suggestion details and comments returned' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
          expect(data['comments']).to be_an(Array)&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is owner of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        # make sure user is set as owner of suggestion&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
        # test for student being able to view their own suggestion&lt;br /&gt;
        response '200', 'student can view their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is not owner of suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
        response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for when suggestion doesn't exist&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (add comment test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
  post 'Add a comment to a suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
    parameter name: :comment, in: :body, schema: {&lt;br /&gt;
      type: :object,&lt;br /&gt;
      properties: {&lt;br /&gt;
        comment: { type: :string }&lt;br /&gt;
      },&lt;br /&gt;
      required: ['comment']&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # successful comment addition test&lt;br /&gt;
    response '201', 'comment_added' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for missing or empty comment&lt;br /&gt;
    response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        # Mock the params to simulate the request&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
        # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
      let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (approve test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
  post 'Approve suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # successful suggestion approval&lt;br /&gt;
      response '200', 'suggestion approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Approved')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable with a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable without a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when a suggestion is not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user as not having ta_privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for students not being able to approve suggestions&lt;br /&gt;
      response '403', 'students cannot approve suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (reject test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
  post 'Reject suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is instructor/ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion rejection&lt;br /&gt;
      response '200', 'suggestion rejected' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Rejected')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion already being approved&lt;br /&gt;
      response '422', 'suggestion already approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulate suggestion status as 'Approved'&lt;br /&gt;
          suggestion.update!(status: 'Approved')&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          parsed_response = JSON.parse(response.body)&lt;br /&gt;
          expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when the suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
      response '403', 'students cannot reject suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (edit/update test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  patch 'update suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion update&lt;br /&gt;
      response '200', 'suggestion updated' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to be a student and setup current user&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is owner/a part of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students can update their own suggestion(s)&lt;br /&gt;
        response '200', 'student can update their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is not owner to suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
        response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not being found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion being invalid in some form/missing&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
        allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (create test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  post 'Create suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
              description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
    # test for successful suggestion creation&lt;br /&gt;
    response '201', 'suggestion created successfully' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Suggestion title',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return properly for test&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
        expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
        expect(data['auto_signup']).to eq(true)&lt;br /&gt;
        expect(data['status']).to eq('Initialized')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for a missing parameter&lt;br /&gt;
    response '422', 'missing title' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: '',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return current user properly&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['error']).to eq('title is missing')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for invalid suggestion given&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Sample Suggestion',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
        allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  delete 'Delete suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion deletion&lt;br /&gt;
      response '204', 'suggestion deleted' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(204)&lt;br /&gt;
          expect(response.body).to be_empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for record not being destroyed&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the deletion process&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion is not found/doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test to make sure students cannot delete suggestions&lt;br /&gt;
      response '403', 'students cannot delete suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (index test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  get 'list all suggestions' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful indexing&lt;br /&gt;
      response '200', 'suggestions listed' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
         run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
          expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test case for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for student to be forbidden from indexing suggestions&lt;br /&gt;
      response '403', 'students cannot index suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Updated Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def login_user&lt;br /&gt;
  # Create a user using the factory&lt;br /&gt;
  user = create(:user)&lt;br /&gt;
&lt;br /&gt;
  # Make a POST request to login&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
&lt;br /&gt;
  # Parse the JSON response and extract the token&lt;br /&gt;
  json_response = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
  # Return the token from the response&lt;br /&gt;
  { token: json_response['token'], user: }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  # Login user, grab token, set user and current user&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    auth_data = login_user&lt;br /&gt;
    @token = auth_data[:token]&lt;br /&gt;
    @user = auth_data[:user]&lt;br /&gt;
    @current_user = @user&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # create default assignment and suggestion&lt;br /&gt;
  let(:assignment) { create(:assignment) }&lt;br /&gt;
  let(:suggestion) { create(:suggestion, assignment_id: assignment.id, user_id: @user.id) }&lt;br /&gt;
&lt;br /&gt;
  # Testing for add_comment method&lt;br /&gt;
  path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
    post 'Add a comment to a suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
      parameter name: :comment, in: :body, schema: {&lt;br /&gt;
        type: :object,&lt;br /&gt;
        properties: {&lt;br /&gt;
          comment: { type: :string }&lt;br /&gt;
        },&lt;br /&gt;
        required: ['comment']&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      # successful comment addition test&lt;br /&gt;
      response '201', 'comment_added' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for missing or empty comment&lt;br /&gt;
      response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Mock the params to simulate the request&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
          # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
        let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Tests for approving suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
    post 'Approve suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # successful suggestion approval&lt;br /&gt;
        response '200', 'suggestion approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Approved')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable with a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable without a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when a suggestion is not found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user as not having ta_privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for students not being able to approve suggestions&lt;br /&gt;
        response '403', 'students cannot approve suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for creating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    post 'Create suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
                description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion creation&lt;br /&gt;
      response '201', 'suggestion created successfully' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Suggestion title',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return properly for test&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
          expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
          expect(data['auto_signup']).to eq(true)&lt;br /&gt;
          expect(data['status']).to eq('Initialized')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for a missing parameter&lt;br /&gt;
      response '422', 'missing title' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: '',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return current user properly&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['error']).to eq('title is missing')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for invalid suggestion given&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Sample Suggestion',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
          suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
          allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for deleting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    delete 'Delete suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion deletion&lt;br /&gt;
        response '204', 'suggestion deleted' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(204)&lt;br /&gt;
            expect(response.body).to be_empty&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for record not being destroyed&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the deletion process&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when suggestion is not found/doesn't exist&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students cannot delete suggestions&lt;br /&gt;
        response '403', 'students cannot delete suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for indexing/listing all suggestions&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    get 'list all suggestions' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
          suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful indexing&lt;br /&gt;
        response '200', 'suggestions listed' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
            expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for student to be forbidden from indexing suggestions&lt;br /&gt;
        response '403', 'students cannot index suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for rejecting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
    post 'Reject suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is instructor/ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion rejection&lt;br /&gt;
        response '200', 'suggestion rejected' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Rejected')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for suggestion already being approved&lt;br /&gt;
        response '422', 'suggestion already approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulate suggestion status as 'Approved'&lt;br /&gt;
            suggestion.update!(status: 'Approved')&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
            parsed_response = JSON.parse(response.body)&lt;br /&gt;
            expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when the suggestion not being found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
        response '403', 'students cannot reject suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for showing suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    get 'show suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when user is a instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
        response '200', 'suggestion details and comments returned' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when user is owner of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          # make sure user is set as owner of suggestion&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
          # test for student being able to view their own suggestion&lt;br /&gt;
          response '200', 'student can view their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              data = JSON.parse(response.body)&lt;br /&gt;
              expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
              expect(data['comments']).to be_an(Array)&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test case for when user is not owner of suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
          response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for updating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    patch 'update suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion update&lt;br /&gt;
        response '200', 'suggestion updated' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to be a student and setup current user&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is owner/a part of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test to make sure students can update their own suggestion(s)&lt;br /&gt;
          response '200', 'student can update their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is not owner to suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
          response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion being invalid in some form/missing&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
          allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Test Coverage===&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add detail to testing&lt;br /&gt;
* Record testing video&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Pranavi Sanganabhatla (psangan@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
* [# Testing video]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160761</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160761"/>
		<updated>2024-12-04T05:48:07Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Test Plan */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
The controller is now fully commented, and this wiki page goes more in-depth about the code, design, and testing.&lt;br /&gt;
&lt;br /&gt;
Various other QoL changes are implemented, such as alphabetized hash parameters and collapsible source code views.&lt;br /&gt;
&lt;br /&gt;
The RSpec file is still under construction.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required: (skip steps 3 and 4 if automatic sign up is turned off)&lt;br /&gt;
## Mark the suggestion as approved&lt;br /&gt;
## Create a new topic from the suggestion&lt;br /&gt;
## Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
## Send the topic approved message&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Action &lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Method name&lt;br /&gt;
! Comment&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; | no change &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | add_comment&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve&lt;br /&gt;
| assimilated approve_suggestion&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | show&lt;br /&gt;
| assimilated student_view&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;5&amp;quot; | added &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create_topic_from_suggestion!&lt;br /&gt;
| chain of helper methods distilled into single call to create&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_student&lt;br /&gt;
| privilege check helper method, only TA and higher can pass&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_non_owner_student&lt;br /&gt;
| privilege check helper method, same as above, but student passes if he is the owner&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | destroy&lt;br /&gt;
| it should be possible to delete a suggestion (D in CRUD)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | send_notice_of_rejection&lt;br /&gt;
| complementary message to approval message&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | removed &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | action_allowed?&lt;br /&gt;
| different methods require different authorization checks, use dedicated helpers instead&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve_suggestion&lt;br /&gt;
| merged into approve&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | new&lt;br /&gt;
| view methods don't apply to JSON APIs&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_edit&lt;br /&gt;
| editing a suggestion is not allowed&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_view&lt;br /&gt;
| merged into show&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | submit&lt;br /&gt;
| call-this-or-that method&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | suggestion_params&lt;br /&gt;
| inlined into before_action declaration&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;6&amp;quot; | renamed &lt;br /&gt;
! Old&lt;br /&gt;
! New&lt;br /&gt;
!&lt;br /&gt;
|-&lt;br /&gt;
| list&lt;br /&gt;
| index&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| notification&lt;br /&gt;
| sign_team_up!&lt;br /&gt;
| method name and implementation didn't match&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion&lt;br /&gt;
| reject&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| send_email&lt;br /&gt;
| send_notice_of_approval&lt;br /&gt;
| method names should be specific&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion&lt;br /&gt;
| update&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code. Additionally, the suggestions controller now notifies the suggester if his suggestion has been rejected instead of just silently updating the state.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
This task specifically targeted the view files. Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
Even so, DRY was observed throughout the project by extracting common statements into helper functions, most notably the privilege check methods.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
===Tables===&lt;br /&gt;
&lt;br /&gt;
The following tables are newly added to the database schema. The question mark at the end of foreign_key indicates that this field is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:tables.png]]&lt;br /&gt;
&lt;br /&gt;
===Model associations===&lt;br /&gt;
&lt;br /&gt;
While simple at first glance, the suggestions controller interacts with many models in the system.&lt;br /&gt;
&lt;br /&gt;
The models in the first row of the diagram are the direct subject material of the controller as most API endpoints perform CRUD operations on only these two. The grayed out models in the second row are only ever read by the controller, they can be considered static throughout any call to the controller. The remaining four models in the last row and column of the diagram are handled by the suggestion approval process. The controller may perform any of the CRUD operations on these models.&lt;br /&gt;
&lt;br /&gt;
The arrows in the diagram point in the direction of ownership, that is, an arrow points from models A to B if A belongs to B. The dashed arrow labeled 'optional' in the legend means that the foreign key is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:ActiveRecord Associations.png]]&lt;br /&gt;
&lt;br /&gt;
===Controller and collaborators===&lt;br /&gt;
&lt;br /&gt;
The following diagram shows the controller, its API endpoints, its collaborators, and the methods accessed from the controller. The AuthorizationHelper module is imported into the controller class.&lt;br /&gt;
&lt;br /&gt;
[[File:classes.png]]&lt;br /&gt;
&lt;br /&gt;
==JSON API==&lt;br /&gt;
&lt;br /&gt;
The backend reimplementation project aims to convert Expertiza into a split frontend-backend service. To this end, the backend will respond with JSON only.&lt;br /&gt;
&lt;br /&gt;
===Request Format===&lt;br /&gt;
&lt;br /&gt;
In addition to the authorization headers, a combination of seven fields may be required for the specific API endpoint. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! rowspan=&amp;quot;2&amp;quot; | Parameter !! colspan=&amp;quot;8&amp;quot; | Included in API Endpoint !! rowspan=&amp;quot;2&amp;quot; | Type !! rowspan=&amp;quot;2&amp;quot; | Description&lt;br /&gt;
|-&lt;br /&gt;
! add_comment !! approve !! create !! destroy !! index !! reject !! show !! update&lt;br /&gt;
|-&lt;br /&gt;
! id&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓* || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || integer || Suggestion ID&lt;br /&gt;
|-&lt;br /&gt;
! anonymous&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || boolean || Whether to nullify user ID of suggester&lt;br /&gt;
|-&lt;br /&gt;
! assignment_id&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || integer || Assignment ID&lt;br /&gt;
|-&lt;br /&gt;
! auto_signup&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || boolean || Whether to sign up team when approved&lt;br /&gt;
|-&lt;br /&gt;
! comment&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || string || SuggestionComment content&lt;br /&gt;
|-&lt;br /&gt;
! description&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion description&lt;br /&gt;
|-&lt;br /&gt;
! title&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion title&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
* *: The id parameter specifies the Assignment ID instead&lt;br /&gt;
* ✓: Field required&lt;br /&gt;
* ✕: Field ignored&lt;br /&gt;
* ~: Field optional&lt;br /&gt;
&lt;br /&gt;
For example, the request data for the add_comment method would be { &amp;quot;id&amp;quot;: 1, &amp;quot;comment&amp;quot;: &amp;quot;This is a comment on one of the suggestions&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
===Status Codes===&lt;br /&gt;
&lt;br /&gt;
Depending on the parameters and database records, different HTTP status codes may be returned. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Code || Mnemonic || Condition || API Endpoints&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 2xx || success || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 200 || ok || Request successful || approve&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&amp;lt;br&amp;gt;(c&amp;lt;strong&amp;gt;RU&amp;lt;/strong&amp;gt;d)&lt;br /&gt;
|-&lt;br /&gt;
| 201 || created || Comment successfully created&amp;lt;br&amp;gt;SuggestionComment successfully created || add_comment&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;(&amp;lt;strong&amp;gt;C&amp;lt;/strong&amp;gt;rud)&lt;br /&gt;
|-&lt;br /&gt;
| 204 || no content || Suggestion successfully deleted || delete&amp;lt;br&amp;gt;(cru&amp;lt;strong&amp;gt;D&amp;lt;/strong&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 4xx || client error || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 403 || forbidden || Privilege check fails || approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 404 || not found || ActiveRecord::RecordNotFound exception || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 422 || unprocessable entity || ActiveRecord::RecordInvalid exception&amp;lt;br&amp;gt;ActiveRecord::RecordNotDestroyed exception&amp;lt;br&amp;gt;Suggestion already approved when trying to reject || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;update&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Response Format===&lt;br /&gt;
&lt;br /&gt;
Depending on the API endpoint of the request, a different entity may be returned. Note that only a success (2xx) will return an entity, a client error (4xx) will return an error message instead. The returned entities are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! API Endpoint || Rendered Model&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || SuggestionComment&lt;br /&gt;
|-&lt;br /&gt;
| approve || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| create || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| destroy || { }&lt;br /&gt;
|-&lt;br /&gt;
| index || Array of Suggestions&lt;br /&gt;
|-&lt;br /&gt;
| reject || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| show || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| update || Suggestion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
Each of the boxes with file names are expandable and contain the contents of the file. If applicable, a before-after view is provided.&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :title, :description, presence: true&lt;br /&gt;
  has_many :suggestion_comments&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :assignment&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
# https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  # Strong params inlined as global before_action&lt;br /&gt;
  before_action except: [:index] do&lt;br /&gt;
    params.require(:id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Comment on a suggestion.&lt;br /&gt;
  # A new SuggestionComment record is made.&lt;br /&gt;
  def add_comment&lt;br /&gt;
    params.require(:comment)&lt;br /&gt;
    Suggestion.find(params[:id])&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Approve a suggestion, even if it was previously rejected.&lt;br /&gt;
  def approve&lt;br /&gt;
    deny_student('Students cannot approve a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Only go through the approval process if the suggestion isn't already approved.&lt;br /&gt;
    unless @suggestion.status == 'Approved'&lt;br /&gt;
      # Since this process updates multiple records at once, wrap them in&lt;br /&gt;
      #   a transaction so that either all of them succeed, or none of them.&lt;br /&gt;
      transaction do&lt;br /&gt;
        # The approval process is:&lt;br /&gt;
        # 1. Mark the suggestion as approved&lt;br /&gt;
        # 2. Create a new topic from the suggestion&lt;br /&gt;
        # 3. Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
        # 4. Send the topic approved message&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        # If a suggestion is anonymous, the suggester is&lt;br /&gt;
        #  unknown and thus steps 3 and 4 can't be taken.&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up!&lt;br /&gt;
          send_notice_of_approval&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # A new Suggestion record is made.&lt;br /&gt;
  def create&lt;br /&gt;
    params.require(%i[anonymous assignment_id auto_signup comment description title])&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      # Anonymous suggestions are allowed by nulling user_id.&lt;br /&gt;
      user_id: params[:anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Delete a suggestion from the records.&lt;br /&gt;
  def destroy&lt;br /&gt;
    deny_student('Students cannot delete suggestions.')&lt;br /&gt;
    Suggestion.find(params[:id]).destroy!&lt;br /&gt;
    render json: {}, status: :no_content # 204&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a list of all Suggestion records associated with a particular assignment.&lt;br /&gt;
  def index&lt;br /&gt;
    deny_student('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
    render json: Suggestion.where(assignment_id: params[:id]), status: :ok # 200&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Reject a suggestion unless it was already approved.&lt;br /&gt;
  def reject&lt;br /&gt;
    deny_student('Students cannot reject a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Since the approval process makes changes to many records,&lt;br /&gt;
    #   rejecting a previously approved suggestion is not possible.&lt;br /&gt;
    if @suggestion.status == 'Approved'&lt;br /&gt;
      render json: { error: 'Suggestion has already been approved.' }, status: :unprocessable_entity # 422&lt;br /&gt;
    elsif @suggestion.status == 'Initialized'&lt;br /&gt;
      # The rejection process is:&lt;br /&gt;
      # 1. Mark the suggestion as rejected&lt;br /&gt;
      # 2. Send the topic rejected message&lt;br /&gt;
      @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      send_notice_of_rejection if @suggestion.user_id&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a single Suggestion record.&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only view their own suggestions.')&lt;br /&gt;
    render json: {&lt;br /&gt;
      comments: SuggestionComment.where(suggestion_id: params[:id]),&lt;br /&gt;
      suggestion: @suggestion&lt;br /&gt;
    }, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Change the details of a Suggestion.&lt;br /&gt;
  def update&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only edit their own suggestions.')&lt;br /&gt;
    # Only title, description, and signup preference can be changed.&lt;br /&gt;
    @suggestion.update!(params.permit(:title, :description, :auto_signup))&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    # Convert a suggestion into a fully fledged topic.&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1,&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_student(err_msg)&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_non_owner_student(err_msg)&lt;br /&gt;
    # If the student owns the resource, they are allowed to perform&lt;br /&gt;
    #   every action related to Suggestions and SuggestionComments.&lt;br /&gt;
    return if @suggestion.user_id == @current_user.id&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval&lt;br /&gt;
    # Email the suggester and CC the suggester's teammates that the suggestion was approved&lt;br /&gt;
    Mailer.send_topic_approved_email(&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      suggester: @suggester,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_rejection&lt;br /&gt;
    # Email the suggester that the suggestion was rejected&lt;br /&gt;
    Mailer.send_topic_rejected_email(&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been rejected&amp;quot;,&lt;br /&gt;
      suggester: User.find(@suggestion.user_id),&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    # Find the suggester's team which is signed up to the topic's assignment.&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    # Create the team if necessary.&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    # If the suggester's team has no assignment, then clear its&lt;br /&gt;
    #   waitlist and sign it up to the newly created topic.&lt;br /&gt;
    unless SignedUpTeam.exists?(team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: true).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    # Since the suggester's team is signed up to the topic, make it private&lt;br /&gt;
    #   to the suggester so no other teams will attempt to sign up to it.&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML.&amp;lt;br&amp;gt;The JSON API is described in [[#JSON API|JSON API]]&lt;br /&gt;
* Switched from check-avoid to fail-recover coding style to minimize number of if statements and thus indentation.&amp;lt;br&amp;gt;As a result, the methods become easier to understand, as the intended behavior is programmed all in one block with each error handled all the way at the end of the method&lt;br /&gt;
* Methods have proper error handling.&amp;lt;br&amp;gt;Each method was given a 'rescue' clause for any error that might occur, and most ActiveRecords methods were switched from the ones that return a boolean to the ones that throw an exception&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects and end in an exclamation point if the method modifies the database&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-segmented into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries.&amp;lt;br&amp;gt;Additionally, a complementary email method for rejections has been addded&lt;br /&gt;
* Where required, API entry points call dedicated privilege check methods&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, cc: defn[:cc], subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_topic_rejected_email(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;authorization_helper.rb (Trimmed)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (authorization_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_approved_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been approved.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you want to work on this topic, you can log into Expertiza and switch topics.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_rejected_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been rejected.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you believe this to be an error, please contact your instructor or one of the TAs.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The authorization helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
The mailer helper is accompanied by its template files for each type of message sent, in this case the topic approved and topic rejected messages. They also exist as plain text template files, since the original repo had both html and plaintext versions of the email templates. The plaintext versions of the template files are not shown as their content is the same, just stripped of the HTML tags.&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the following section:&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (show test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  get 'show suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when user is a instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
      response '200', 'suggestion details and comments returned' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
          expect(data['comments']).to be_an(Array)&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is owner of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        # make sure user is set as owner of suggestion&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
        # test for student being able to view their own suggestion&lt;br /&gt;
        response '200', 'student can view their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is not owner of suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
        response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for when suggestion doesn't exist&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (add comment test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
  post 'Add a comment to a suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
    parameter name: :comment, in: :body, schema: {&lt;br /&gt;
      type: :object,&lt;br /&gt;
      properties: {&lt;br /&gt;
        comment: { type: :string }&lt;br /&gt;
      },&lt;br /&gt;
      required: ['comment']&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # successful comment addition test&lt;br /&gt;
    response '201', 'comment_added' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for missing or empty comment&lt;br /&gt;
    response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        # Mock the params to simulate the request&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
        # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
      let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (approve test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
  post 'Approve suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # successful suggestion approval&lt;br /&gt;
      response '200', 'suggestion approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Approved')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable with a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable without a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when a suggestion is not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user as not having ta_privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for students not being able to approve suggestions&lt;br /&gt;
      response '403', 'students cannot approve suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (reject test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
  post 'Reject suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is instructor/ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion rejection&lt;br /&gt;
      response '200', 'suggestion rejected' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Rejected')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion already being approved&lt;br /&gt;
      response '422', 'suggestion already approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulate suggestion status as 'Approved'&lt;br /&gt;
          suggestion.update!(status: 'Approved')&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          parsed_response = JSON.parse(response.body)&lt;br /&gt;
          expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when the suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
      response '403', 'students cannot reject suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (edit/update test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  patch 'update suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion update&lt;br /&gt;
      response '200', 'suggestion updated' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to be a student and setup current user&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is owner/a part of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students can update their own suggestion(s)&lt;br /&gt;
        response '200', 'student can update their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is not owner to suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
        response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not being found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion being invalid in some form/missing&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
        allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (create test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  post 'Create suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
              description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
    # test for successful suggestion creation&lt;br /&gt;
    response '201', 'suggestion created successfully' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Suggestion title',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return properly for test&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
        expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
        expect(data['auto_signup']).to eq(true)&lt;br /&gt;
        expect(data['status']).to eq('Initialized')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for a missing parameter&lt;br /&gt;
    response '422', 'missing title' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: '',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return current user properly&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['error']).to eq('title is missing')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for invalid suggestion given&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Sample Suggestion',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
        allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  delete 'Delete suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion deletion&lt;br /&gt;
      response '204', 'suggestion deleted' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(204)&lt;br /&gt;
          expect(response.body).to be_empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for record not being destroyed&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the deletion process&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion is not found/doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test to make sure students cannot delete suggestions&lt;br /&gt;
      response '403', 'students cannot delete suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (index test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  get 'list all suggestions' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful indexing&lt;br /&gt;
      response '200', 'suggestions listed' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
         run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
          expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test case for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for student to be forbidden from indexing suggestions&lt;br /&gt;
      response '403', 'students cannot index suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Updated Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def login_user&lt;br /&gt;
  # Create a user using the factory&lt;br /&gt;
  user = create(:user)&lt;br /&gt;
&lt;br /&gt;
  # Make a POST request to login&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
&lt;br /&gt;
  # Parse the JSON response and extract the token&lt;br /&gt;
  json_response = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
  # Return the token from the response&lt;br /&gt;
  { token: json_response['token'], user: }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  # Login user, grab token, set user and current user&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    auth_data = login_user&lt;br /&gt;
    @token = auth_data[:token]&lt;br /&gt;
    @user = auth_data[:user]&lt;br /&gt;
    @current_user = @user&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # create default assignment and suggestion&lt;br /&gt;
  let(:assignment) { create(:assignment) }&lt;br /&gt;
  let(:suggestion) { create(:suggestion, assignment_id: assignment.id, user_id: @user.id) }&lt;br /&gt;
&lt;br /&gt;
  # Testing for add_comment method&lt;br /&gt;
  path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
    post 'Add a comment to a suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
      parameter name: :comment, in: :body, schema: {&lt;br /&gt;
        type: :object,&lt;br /&gt;
        properties: {&lt;br /&gt;
          comment: { type: :string }&lt;br /&gt;
        },&lt;br /&gt;
        required: ['comment']&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      # successful comment addition test&lt;br /&gt;
      response '201', 'comment_added' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for missing or empty comment&lt;br /&gt;
      response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Mock the params to simulate the request&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
          # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
        let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Tests for approving suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
    post 'Approve suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # successful suggestion approval&lt;br /&gt;
        response '200', 'suggestion approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Approved')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable with a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable without a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when a suggestion is not found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user as not having ta_privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for students not being able to approve suggestions&lt;br /&gt;
        response '403', 'students cannot approve suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for creating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    post 'Create suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
                description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion creation&lt;br /&gt;
      response '201', 'suggestion created successfully' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Suggestion title',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return properly for test&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
          expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
          expect(data['auto_signup']).to eq(true)&lt;br /&gt;
          expect(data['status']).to eq('Initialized')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for a missing parameter&lt;br /&gt;
      response '422', 'missing title' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: '',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return current user properly&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['error']).to eq('title is missing')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for invalid suggestion given&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Sample Suggestion',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
          suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
          allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for deleting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    delete 'Delete suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion deletion&lt;br /&gt;
        response '204', 'suggestion deleted' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(204)&lt;br /&gt;
            expect(response.body).to be_empty&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for record not being destroyed&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the deletion process&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when suggestion is not found/doesn't exist&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students cannot delete suggestions&lt;br /&gt;
        response '403', 'students cannot delete suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for indexing/listing all suggestions&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    get 'list all suggestions' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
          suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful indexing&lt;br /&gt;
        response '200', 'suggestions listed' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
            expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for student to be forbidden from indexing suggestions&lt;br /&gt;
        response '403', 'students cannot index suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for rejecting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
    post 'Reject suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is instructor/ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion rejection&lt;br /&gt;
        response '200', 'suggestion rejected' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Rejected')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for suggestion already being approved&lt;br /&gt;
        response '422', 'suggestion already approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulate suggestion status as 'Approved'&lt;br /&gt;
            suggestion.update!(status: 'Approved')&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
            parsed_response = JSON.parse(response.body)&lt;br /&gt;
            expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when the suggestion not being found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
        response '403', 'students cannot reject suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for showing suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    get 'show suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when user is a instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
        response '200', 'suggestion details and comments returned' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when user is owner of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          # make sure user is set as owner of suggestion&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
          # test for student being able to view their own suggestion&lt;br /&gt;
          response '200', 'student can view their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              data = JSON.parse(response.body)&lt;br /&gt;
              expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
              expect(data['comments']).to be_an(Array)&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test case for when user is not owner of suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
          response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for updating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    patch 'update suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion update&lt;br /&gt;
        response '200', 'suggestion updated' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to be a student and setup current user&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is owner/a part of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test to make sure students can update their own suggestion(s)&lt;br /&gt;
          response '200', 'student can update their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is not owner to suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
          response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion being invalid in some form/missing&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
          allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Test coverage===&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add detail to testing&lt;br /&gt;
* Record testing video&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Pranavi Sanganabhatla (psangan@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
* [# Testing video]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160760</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160760"/>
		<updated>2024-12-04T05:46:51Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Detailed Test Plan */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
The controller is now fully commented, and this wiki page goes more in-depth about the code, design, and testing.&lt;br /&gt;
&lt;br /&gt;
Various other QoL changes are implemented, such as alphabetized hash parameters and collapsible source code views.&lt;br /&gt;
&lt;br /&gt;
The RSpec file is still under construction.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required: (skip steps 3 and 4 if automatic sign up is turned off)&lt;br /&gt;
## Mark the suggestion as approved&lt;br /&gt;
## Create a new topic from the suggestion&lt;br /&gt;
## Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
## Send the topic approved message&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Action &lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Method name&lt;br /&gt;
! Comment&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; | no change &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | add_comment&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve&lt;br /&gt;
| assimilated approve_suggestion&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | show&lt;br /&gt;
| assimilated student_view&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;5&amp;quot; | added &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create_topic_from_suggestion!&lt;br /&gt;
| chain of helper methods distilled into single call to create&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_student&lt;br /&gt;
| privilege check helper method, only TA and higher can pass&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_non_owner_student&lt;br /&gt;
| privilege check helper method, same as above, but student passes if he is the owner&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | destroy&lt;br /&gt;
| it should be possible to delete a suggestion (D in CRUD)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | send_notice_of_rejection&lt;br /&gt;
| complementary message to approval message&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | removed &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | action_allowed?&lt;br /&gt;
| different methods require different authorization checks, use dedicated helpers instead&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve_suggestion&lt;br /&gt;
| merged into approve&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | new&lt;br /&gt;
| view methods don't apply to JSON APIs&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_edit&lt;br /&gt;
| editing a suggestion is not allowed&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_view&lt;br /&gt;
| merged into show&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | submit&lt;br /&gt;
| call-this-or-that method&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | suggestion_params&lt;br /&gt;
| inlined into before_action declaration&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;6&amp;quot; | renamed &lt;br /&gt;
! Old&lt;br /&gt;
! New&lt;br /&gt;
!&lt;br /&gt;
|-&lt;br /&gt;
| list&lt;br /&gt;
| index&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| notification&lt;br /&gt;
| sign_team_up!&lt;br /&gt;
| method name and implementation didn't match&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion&lt;br /&gt;
| reject&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| send_email&lt;br /&gt;
| send_notice_of_approval&lt;br /&gt;
| method names should be specific&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion&lt;br /&gt;
| update&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code. Additionally, the suggestions controller now notifies the suggester if his suggestion has been rejected instead of just silently updating the state.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
This task specifically targeted the view files. Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
Even so, DRY was observed throughout the project by extracting common statements into helper functions, most notably the privilege check methods.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
===Tables===&lt;br /&gt;
&lt;br /&gt;
The following tables are newly added to the database schema. The question mark at the end of foreign_key indicates that this field is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:tables.png]]&lt;br /&gt;
&lt;br /&gt;
===Model associations===&lt;br /&gt;
&lt;br /&gt;
While simple at first glance, the suggestions controller interacts with many models in the system.&lt;br /&gt;
&lt;br /&gt;
The models in the first row of the diagram are the direct subject material of the controller as most API endpoints perform CRUD operations on only these two. The grayed out models in the second row are only ever read by the controller, they can be considered static throughout any call to the controller. The remaining four models in the last row and column of the diagram are handled by the suggestion approval process. The controller may perform any of the CRUD operations on these models.&lt;br /&gt;
&lt;br /&gt;
The arrows in the diagram point in the direction of ownership, that is, an arrow points from models A to B if A belongs to B. The dashed arrow labeled 'optional' in the legend means that the foreign key is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:ActiveRecord Associations.png]]&lt;br /&gt;
&lt;br /&gt;
===Controller and collaborators===&lt;br /&gt;
&lt;br /&gt;
The following diagram shows the controller, its API endpoints, its collaborators, and the methods accessed from the controller. The AuthorizationHelper module is imported into the controller class.&lt;br /&gt;
&lt;br /&gt;
[[File:classes.png]]&lt;br /&gt;
&lt;br /&gt;
==JSON API==&lt;br /&gt;
&lt;br /&gt;
The backend reimplementation project aims to convert Expertiza into a split frontend-backend service. To this end, the backend will respond with JSON only.&lt;br /&gt;
&lt;br /&gt;
===Request Format===&lt;br /&gt;
&lt;br /&gt;
In addition to the authorization headers, a combination of seven fields may be required for the specific API endpoint. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! rowspan=&amp;quot;2&amp;quot; | Parameter !! colspan=&amp;quot;8&amp;quot; | Included in API Endpoint !! rowspan=&amp;quot;2&amp;quot; | Type !! rowspan=&amp;quot;2&amp;quot; | Description&lt;br /&gt;
|-&lt;br /&gt;
! add_comment !! approve !! create !! destroy !! index !! reject !! show !! update&lt;br /&gt;
|-&lt;br /&gt;
! id&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓* || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || integer || Suggestion ID&lt;br /&gt;
|-&lt;br /&gt;
! anonymous&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || boolean || Whether to nullify user ID of suggester&lt;br /&gt;
|-&lt;br /&gt;
! assignment_id&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || integer || Assignment ID&lt;br /&gt;
|-&lt;br /&gt;
! auto_signup&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || boolean || Whether to sign up team when approved&lt;br /&gt;
|-&lt;br /&gt;
! comment&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || string || SuggestionComment content&lt;br /&gt;
|-&lt;br /&gt;
! description&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion description&lt;br /&gt;
|-&lt;br /&gt;
! title&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion title&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
* *: The id parameter specifies the Assignment ID instead&lt;br /&gt;
* ✓: Field required&lt;br /&gt;
* ✕: Field ignored&lt;br /&gt;
* ~: Field optional&lt;br /&gt;
&lt;br /&gt;
For example, the request data for the add_comment method would be { &amp;quot;id&amp;quot;: 1, &amp;quot;comment&amp;quot;: &amp;quot;This is a comment on one of the suggestions&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
===Status Codes===&lt;br /&gt;
&lt;br /&gt;
Depending on the parameters and database records, different HTTP status codes may be returned. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Code || Mnemonic || Condition || API Endpoints&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 2xx || success || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 200 || ok || Request successful || approve&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&amp;lt;br&amp;gt;(c&amp;lt;strong&amp;gt;RU&amp;lt;/strong&amp;gt;d)&lt;br /&gt;
|-&lt;br /&gt;
| 201 || created || Comment successfully created&amp;lt;br&amp;gt;SuggestionComment successfully created || add_comment&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;(&amp;lt;strong&amp;gt;C&amp;lt;/strong&amp;gt;rud)&lt;br /&gt;
|-&lt;br /&gt;
| 204 || no content || Suggestion successfully deleted || delete&amp;lt;br&amp;gt;(cru&amp;lt;strong&amp;gt;D&amp;lt;/strong&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 4xx || client error || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 403 || forbidden || Privilege check fails || approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 404 || not found || ActiveRecord::RecordNotFound exception || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 422 || unprocessable entity || ActiveRecord::RecordInvalid exception&amp;lt;br&amp;gt;ActiveRecord::RecordNotDestroyed exception&amp;lt;br&amp;gt;Suggestion already approved when trying to reject || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;update&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Response Format===&lt;br /&gt;
&lt;br /&gt;
Depending on the API endpoint of the request, a different entity may be returned. Note that only a success (2xx) will return an entity, a client error (4xx) will return an error message instead. The returned entities are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! API Endpoint || Rendered Model&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || SuggestionComment&lt;br /&gt;
|-&lt;br /&gt;
| approve || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| create || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| destroy || { }&lt;br /&gt;
|-&lt;br /&gt;
| index || Array of Suggestions&lt;br /&gt;
|-&lt;br /&gt;
| reject || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| show || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| update || Suggestion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
Each of the boxes with file names are expandable and contain the contents of the file. If applicable, a before-after view is provided.&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :title, :description, presence: true&lt;br /&gt;
  has_many :suggestion_comments&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :assignment&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
# https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  # Strong params inlined as global before_action&lt;br /&gt;
  before_action except: [:index] do&lt;br /&gt;
    params.require(:id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Comment on a suggestion.&lt;br /&gt;
  # A new SuggestionComment record is made.&lt;br /&gt;
  def add_comment&lt;br /&gt;
    params.require(:comment)&lt;br /&gt;
    Suggestion.find(params[:id])&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Approve a suggestion, even if it was previously rejected.&lt;br /&gt;
  def approve&lt;br /&gt;
    deny_student('Students cannot approve a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Only go through the approval process if the suggestion isn't already approved.&lt;br /&gt;
    unless @suggestion.status == 'Approved'&lt;br /&gt;
      # Since this process updates multiple records at once, wrap them in&lt;br /&gt;
      #   a transaction so that either all of them succeed, or none of them.&lt;br /&gt;
      transaction do&lt;br /&gt;
        # The approval process is:&lt;br /&gt;
        # 1. Mark the suggestion as approved&lt;br /&gt;
        # 2. Create a new topic from the suggestion&lt;br /&gt;
        # 3. Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
        # 4. Send the topic approved message&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        # If a suggestion is anonymous, the suggester is&lt;br /&gt;
        #  unknown and thus steps 3 and 4 can't be taken.&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up!&lt;br /&gt;
          send_notice_of_approval&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # A new Suggestion record is made.&lt;br /&gt;
  def create&lt;br /&gt;
    params.require(%i[anonymous assignment_id auto_signup comment description title])&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      # Anonymous suggestions are allowed by nulling user_id.&lt;br /&gt;
      user_id: params[:anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Delete a suggestion from the records.&lt;br /&gt;
  def destroy&lt;br /&gt;
    deny_student('Students cannot delete suggestions.')&lt;br /&gt;
    Suggestion.find(params[:id]).destroy!&lt;br /&gt;
    render json: {}, status: :no_content # 204&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a list of all Suggestion records associated with a particular assignment.&lt;br /&gt;
  def index&lt;br /&gt;
    deny_student('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
    render json: Suggestion.where(assignment_id: params[:id]), status: :ok # 200&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Reject a suggestion unless it was already approved.&lt;br /&gt;
  def reject&lt;br /&gt;
    deny_student('Students cannot reject a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Since the approval process makes changes to many records,&lt;br /&gt;
    #   rejecting a previously approved suggestion is not possible.&lt;br /&gt;
    if @suggestion.status == 'Approved'&lt;br /&gt;
      render json: { error: 'Suggestion has already been approved.' }, status: :unprocessable_entity # 422&lt;br /&gt;
    elsif @suggestion.status == 'Initialized'&lt;br /&gt;
      # The rejection process is:&lt;br /&gt;
      # 1. Mark the suggestion as rejected&lt;br /&gt;
      # 2. Send the topic rejected message&lt;br /&gt;
      @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      send_notice_of_rejection if @suggestion.user_id&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a single Suggestion record.&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only view their own suggestions.')&lt;br /&gt;
    render json: {&lt;br /&gt;
      comments: SuggestionComment.where(suggestion_id: params[:id]),&lt;br /&gt;
      suggestion: @suggestion&lt;br /&gt;
    }, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Change the details of a Suggestion.&lt;br /&gt;
  def update&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only edit their own suggestions.')&lt;br /&gt;
    # Only title, description, and signup preference can be changed.&lt;br /&gt;
    @suggestion.update!(params.permit(:title, :description, :auto_signup))&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    # Convert a suggestion into a fully fledged topic.&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1,&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_student(err_msg)&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_non_owner_student(err_msg)&lt;br /&gt;
    # If the student owns the resource, they are allowed to perform&lt;br /&gt;
    #   every action related to Suggestions and SuggestionComments.&lt;br /&gt;
    return if @suggestion.user_id == @current_user.id&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval&lt;br /&gt;
    # Email the suggester and CC the suggester's teammates that the suggestion was approved&lt;br /&gt;
    Mailer.send_topic_approved_email(&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      suggester: @suggester,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_rejection&lt;br /&gt;
    # Email the suggester that the suggestion was rejected&lt;br /&gt;
    Mailer.send_topic_rejected_email(&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been rejected&amp;quot;,&lt;br /&gt;
      suggester: User.find(@suggestion.user_id),&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    # Find the suggester's team which is signed up to the topic's assignment.&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    # Create the team if necessary.&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    # If the suggester's team has no assignment, then clear its&lt;br /&gt;
    #   waitlist and sign it up to the newly created topic.&lt;br /&gt;
    unless SignedUpTeam.exists?(team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: true).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    # Since the suggester's team is signed up to the topic, make it private&lt;br /&gt;
    #   to the suggester so no other teams will attempt to sign up to it.&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML.&amp;lt;br&amp;gt;The JSON API is described in [[#JSON API|JSON API]]&lt;br /&gt;
* Switched from check-avoid to fail-recover coding style to minimize number of if statements and thus indentation.&amp;lt;br&amp;gt;As a result, the methods become easier to understand, as the intended behavior is programmed all in one block with each error handled all the way at the end of the method&lt;br /&gt;
* Methods have proper error handling.&amp;lt;br&amp;gt;Each method was given a 'rescue' clause for any error that might occur, and most ActiveRecords methods were switched from the ones that return a boolean to the ones that throw an exception&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects and end in an exclamation point if the method modifies the database&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-segmented into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries.&amp;lt;br&amp;gt;Additionally, a complementary email method for rejections has been addded&lt;br /&gt;
* Where required, API entry points call dedicated privilege check methods&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, cc: defn[:cc], subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_topic_rejected_email(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;authorization_helper.rb (Trimmed)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (authorization_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_approved_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been approved.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you want to work on this topic, you can log into Expertiza and switch topics.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_rejected_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been rejected.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you believe this to be an error, please contact your instructor or one of the TAs.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The authorization helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
The mailer helper is accompanied by its template files for each type of message sent, in this case the topic approved and topic rejected messages. They also exist as plain text template files, since the original repo had both html and plaintext versions of the email templates. The plaintext versions of the template files are not shown as their content is the same, just stripped of the HTML tags.&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the following section:&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (show test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  get 'show suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when user is a instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
      response '200', 'suggestion details and comments returned' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
          expect(data['comments']).to be_an(Array)&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is owner of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        # make sure user is set as owner of suggestion&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
        # test for student being able to view their own suggestion&lt;br /&gt;
        response '200', 'student can view their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is not owner of suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
        response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for when suggestion doesn't exist&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (add comment test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
  post 'Add a comment to a suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
    parameter name: :comment, in: :body, schema: {&lt;br /&gt;
      type: :object,&lt;br /&gt;
      properties: {&lt;br /&gt;
        comment: { type: :string }&lt;br /&gt;
      },&lt;br /&gt;
      required: ['comment']&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # successful comment addition test&lt;br /&gt;
    response '201', 'comment_added' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for missing or empty comment&lt;br /&gt;
    response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        # Mock the params to simulate the request&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
        # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
      let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (approve test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
  post 'Approve suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # successful suggestion approval&lt;br /&gt;
      response '200', 'suggestion approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Approved')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable with a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable without a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when a suggestion is not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user as not having ta_privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for students not being able to approve suggestions&lt;br /&gt;
      response '403', 'students cannot approve suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (reject test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
  post 'Reject suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is instructor/ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion rejection&lt;br /&gt;
      response '200', 'suggestion rejected' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Rejected')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion already being approved&lt;br /&gt;
      response '422', 'suggestion already approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulate suggestion status as 'Approved'&lt;br /&gt;
          suggestion.update!(status: 'Approved')&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          parsed_response = JSON.parse(response.body)&lt;br /&gt;
          expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when the suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
      response '403', 'students cannot reject suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (edit/update test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  patch 'update suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion update&lt;br /&gt;
      response '200', 'suggestion updated' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to be a student and setup current user&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is owner/a part of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students can update their own suggestion(s)&lt;br /&gt;
        response '200', 'student can update their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is not owner to suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
        response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not being found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion being invalid in some form/missing&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
        allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (create test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  post 'Create suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
              description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
    # test for successful suggestion creation&lt;br /&gt;
    response '201', 'suggestion created successfully' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Suggestion title',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return properly for test&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
        expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
        expect(data['auto_signup']).to eq(true)&lt;br /&gt;
        expect(data['status']).to eq('Initialized')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for a missing parameter&lt;br /&gt;
    response '422', 'missing title' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: '',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return current user properly&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['error']).to eq('title is missing')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for invalid suggestion given&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Sample Suggestion',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
        allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  delete 'Delete suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion deletion&lt;br /&gt;
      response '204', 'suggestion deleted' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(204)&lt;br /&gt;
          expect(response.body).to be_empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for record not being destroyed&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the deletion process&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion is not found/doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test to make sure students cannot delete suggestions&lt;br /&gt;
      response '403', 'students cannot delete suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (index test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  get 'list all suggestions' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful indexing&lt;br /&gt;
      response '200', 'suggestions listed' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
         run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
          expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test case for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for student to be forbidden from indexing suggestions&lt;br /&gt;
      response '403', 'students cannot index suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Updated Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def login_user&lt;br /&gt;
  # Create a user using the factory&lt;br /&gt;
  user = create(:user)&lt;br /&gt;
&lt;br /&gt;
  # Make a POST request to login&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
&lt;br /&gt;
  # Parse the JSON response and extract the token&lt;br /&gt;
  json_response = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
  # Return the token from the response&lt;br /&gt;
  { token: json_response['token'], user: }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  # Login user, grab token, set user and current user&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    auth_data = login_user&lt;br /&gt;
    @token = auth_data[:token]&lt;br /&gt;
    @user = auth_data[:user]&lt;br /&gt;
    @current_user = @user&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # create default assignment and suggestion&lt;br /&gt;
  let(:assignment) { create(:assignment) }&lt;br /&gt;
  let(:suggestion) { create(:suggestion, assignment_id: assignment.id, user_id: @user.id) }&lt;br /&gt;
&lt;br /&gt;
  # Testing for add_comment method&lt;br /&gt;
  path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
    post 'Add a comment to a suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
      parameter name: :comment, in: :body, schema: {&lt;br /&gt;
        type: :object,&lt;br /&gt;
        properties: {&lt;br /&gt;
          comment: { type: :string }&lt;br /&gt;
        },&lt;br /&gt;
        required: ['comment']&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      # successful comment addition test&lt;br /&gt;
      response '201', 'comment_added' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for missing or empty comment&lt;br /&gt;
      response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Mock the params to simulate the request&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
          # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
        let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Tests for approving suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
    post 'Approve suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # successful suggestion approval&lt;br /&gt;
        response '200', 'suggestion approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Approved')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable with a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable without a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when a suggestion is not found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user as not having ta_privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for students not being able to approve suggestions&lt;br /&gt;
        response '403', 'students cannot approve suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for creating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    post 'Create suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
                description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion creation&lt;br /&gt;
      response '201', 'suggestion created successfully' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Suggestion title',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return properly for test&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
          expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
          expect(data['auto_signup']).to eq(true)&lt;br /&gt;
          expect(data['status']).to eq('Initialized')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for a missing parameter&lt;br /&gt;
      response '422', 'missing title' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: '',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return current user properly&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['error']).to eq('title is missing')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for invalid suggestion given&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Sample Suggestion',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
          suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
          allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for deleting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    delete 'Delete suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion deletion&lt;br /&gt;
        response '204', 'suggestion deleted' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(204)&lt;br /&gt;
            expect(response.body).to be_empty&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for record not being destroyed&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the deletion process&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when suggestion is not found/doesn't exist&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students cannot delete suggestions&lt;br /&gt;
        response '403', 'students cannot delete suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for indexing/listing all suggestions&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    get 'list all suggestions' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
          suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful indexing&lt;br /&gt;
        response '200', 'suggestions listed' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
            expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for student to be forbidden from indexing suggestions&lt;br /&gt;
        response '403', 'students cannot index suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for rejecting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
    post 'Reject suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is instructor/ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion rejection&lt;br /&gt;
        response '200', 'suggestion rejected' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Rejected')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for suggestion already being approved&lt;br /&gt;
        response '422', 'suggestion already approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulate suggestion status as 'Approved'&lt;br /&gt;
            suggestion.update!(status: 'Approved')&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
            parsed_response = JSON.parse(response.body)&lt;br /&gt;
            expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when the suggestion not being found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
        response '403', 'students cannot reject suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for showing suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    get 'show suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when user is a instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
        response '200', 'suggestion details and comments returned' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when user is owner of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          # make sure user is set as owner of suggestion&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
          # test for student being able to view their own suggestion&lt;br /&gt;
          response '200', 'student can view their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              data = JSON.parse(response.body)&lt;br /&gt;
              expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
              expect(data['comments']).to be_an(Array)&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test case for when user is not owner of suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
          response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for updating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    patch 'update suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion update&lt;br /&gt;
        response '200', 'suggestion updated' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to be a student and setup current user&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is owner/a part of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test to make sure students can update their own suggestion(s)&lt;br /&gt;
          response '200', 'student can update their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is not owner to suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
          response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion being invalid in some form/missing&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
          allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add detail to testing&lt;br /&gt;
* Record testing video&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Pranavi Sanganabhatla (psangan@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
* [# Testing video]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160759</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160759"/>
		<updated>2024-12-04T05:42:46Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Detailed Test Plan */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
The controller is now fully commented, and this wiki page goes more in-depth about the code, design, and testing.&lt;br /&gt;
&lt;br /&gt;
Various other QoL changes are implemented, such as alphabetized hash parameters and collapsible source code views.&lt;br /&gt;
&lt;br /&gt;
The RSpec file is still under construction.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required: (skip steps 3 and 4 if automatic sign up is turned off)&lt;br /&gt;
## Mark the suggestion as approved&lt;br /&gt;
## Create a new topic from the suggestion&lt;br /&gt;
## Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
## Send the topic approved message&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Action &lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Method name&lt;br /&gt;
! Comment&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; | no change &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | add_comment&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve&lt;br /&gt;
| assimilated approve_suggestion&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | show&lt;br /&gt;
| assimilated student_view&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;5&amp;quot; | added &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create_topic_from_suggestion!&lt;br /&gt;
| chain of helper methods distilled into single call to create&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_student&lt;br /&gt;
| privilege check helper method, only TA and higher can pass&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_non_owner_student&lt;br /&gt;
| privilege check helper method, same as above, but student passes if he is the owner&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | destroy&lt;br /&gt;
| it should be possible to delete a suggestion (D in CRUD)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | send_notice_of_rejection&lt;br /&gt;
| complementary message to approval message&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | removed &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | action_allowed?&lt;br /&gt;
| different methods require different authorization checks, use dedicated helpers instead&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve_suggestion&lt;br /&gt;
| merged into approve&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | new&lt;br /&gt;
| view methods don't apply to JSON APIs&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_edit&lt;br /&gt;
| editing a suggestion is not allowed&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_view&lt;br /&gt;
| merged into show&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | submit&lt;br /&gt;
| call-this-or-that method&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | suggestion_params&lt;br /&gt;
| inlined into before_action declaration&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;6&amp;quot; | renamed &lt;br /&gt;
! Old&lt;br /&gt;
! New&lt;br /&gt;
!&lt;br /&gt;
|-&lt;br /&gt;
| list&lt;br /&gt;
| index&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| notification&lt;br /&gt;
| sign_team_up!&lt;br /&gt;
| method name and implementation didn't match&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion&lt;br /&gt;
| reject&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| send_email&lt;br /&gt;
| send_notice_of_approval&lt;br /&gt;
| method names should be specific&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion&lt;br /&gt;
| update&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code. Additionally, the suggestions controller now notifies the suggester if his suggestion has been rejected instead of just silently updating the state.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
This task specifically targeted the view files. Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
Even so, DRY was observed throughout the project by extracting common statements into helper functions, most notably the privilege check methods.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
===Tables===&lt;br /&gt;
&lt;br /&gt;
The following tables are newly added to the database schema. The question mark at the end of foreign_key indicates that this field is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:tables.png]]&lt;br /&gt;
&lt;br /&gt;
===Model associations===&lt;br /&gt;
&lt;br /&gt;
While simple at first glance, the suggestions controller interacts with many models in the system.&lt;br /&gt;
&lt;br /&gt;
The models in the first row of the diagram are the direct subject material of the controller as most API endpoints perform CRUD operations on only these two. The grayed out models in the second row are only ever read by the controller, they can be considered static throughout any call to the controller. The remaining four models in the last row and column of the diagram are handled by the suggestion approval process. The controller may perform any of the CRUD operations on these models.&lt;br /&gt;
&lt;br /&gt;
The arrows in the diagram point in the direction of ownership, that is, an arrow points from models A to B if A belongs to B. The dashed arrow labeled 'optional' in the legend means that the foreign key is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:ActiveRecord Associations.png]]&lt;br /&gt;
&lt;br /&gt;
===Controller and collaborators===&lt;br /&gt;
&lt;br /&gt;
The following diagram shows the controller, its API endpoints, its collaborators, and the methods accessed from the controller. The AuthorizationHelper module is imported into the controller class.&lt;br /&gt;
&lt;br /&gt;
[[File:classes.png]]&lt;br /&gt;
&lt;br /&gt;
==JSON API==&lt;br /&gt;
&lt;br /&gt;
The backend reimplementation project aims to convert Expertiza into a split frontend-backend service. To this end, the backend will respond with JSON only.&lt;br /&gt;
&lt;br /&gt;
===Request Format===&lt;br /&gt;
&lt;br /&gt;
In addition to the authorization headers, a combination of seven fields may be required for the specific API endpoint. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! rowspan=&amp;quot;2&amp;quot; | Parameter !! colspan=&amp;quot;8&amp;quot; | Included in API Endpoint !! rowspan=&amp;quot;2&amp;quot; | Type !! rowspan=&amp;quot;2&amp;quot; | Description&lt;br /&gt;
|-&lt;br /&gt;
! add_comment !! approve !! create !! destroy !! index !! reject !! show !! update&lt;br /&gt;
|-&lt;br /&gt;
! id&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓* || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || integer || Suggestion ID&lt;br /&gt;
|-&lt;br /&gt;
! anonymous&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || boolean || Whether to nullify user ID of suggester&lt;br /&gt;
|-&lt;br /&gt;
! assignment_id&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || integer || Assignment ID&lt;br /&gt;
|-&lt;br /&gt;
! auto_signup&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || boolean || Whether to sign up team when approved&lt;br /&gt;
|-&lt;br /&gt;
! comment&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || string || SuggestionComment content&lt;br /&gt;
|-&lt;br /&gt;
! description&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion description&lt;br /&gt;
|-&lt;br /&gt;
! title&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion title&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
* *: The id parameter specifies the Assignment ID instead&lt;br /&gt;
* ✓: Field required&lt;br /&gt;
* ✕: Field ignored&lt;br /&gt;
* ~: Field optional&lt;br /&gt;
&lt;br /&gt;
For example, the request data for the add_comment method would be { &amp;quot;id&amp;quot;: 1, &amp;quot;comment&amp;quot;: &amp;quot;This is a comment on one of the suggestions&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
===Status Codes===&lt;br /&gt;
&lt;br /&gt;
Depending on the parameters and database records, different HTTP status codes may be returned. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Code || Mnemonic || Condition || API Endpoints&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 2xx || success || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 200 || ok || Request successful || approve&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&amp;lt;br&amp;gt;(c&amp;lt;strong&amp;gt;RU&amp;lt;/strong&amp;gt;d)&lt;br /&gt;
|-&lt;br /&gt;
| 201 || created || Comment successfully created&amp;lt;br&amp;gt;SuggestionComment successfully created || add_comment&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;(&amp;lt;strong&amp;gt;C&amp;lt;/strong&amp;gt;rud)&lt;br /&gt;
|-&lt;br /&gt;
| 204 || no content || Suggestion successfully deleted || delete&amp;lt;br&amp;gt;(cru&amp;lt;strong&amp;gt;D&amp;lt;/strong&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 4xx || client error || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 403 || forbidden || Privilege check fails || approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 404 || not found || ActiveRecord::RecordNotFound exception || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 422 || unprocessable entity || ActiveRecord::RecordInvalid exception&amp;lt;br&amp;gt;ActiveRecord::RecordNotDestroyed exception&amp;lt;br&amp;gt;Suggestion already approved when trying to reject || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;update&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Response Format===&lt;br /&gt;
&lt;br /&gt;
Depending on the API endpoint of the request, a different entity may be returned. Note that only a success (2xx) will return an entity, a client error (4xx) will return an error message instead. The returned entities are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! API Endpoint || Rendered Model&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || SuggestionComment&lt;br /&gt;
|-&lt;br /&gt;
| approve || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| create || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| destroy || { }&lt;br /&gt;
|-&lt;br /&gt;
| index || Array of Suggestions&lt;br /&gt;
|-&lt;br /&gt;
| reject || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| show || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| update || Suggestion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
Each of the boxes with file names are expandable and contain the contents of the file. If applicable, a before-after view is provided.&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :title, :description, presence: true&lt;br /&gt;
  has_many :suggestion_comments&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :assignment&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
# https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  # Strong params inlined as global before_action&lt;br /&gt;
  before_action except: [:index] do&lt;br /&gt;
    params.require(:id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Comment on a suggestion.&lt;br /&gt;
  # A new SuggestionComment record is made.&lt;br /&gt;
  def add_comment&lt;br /&gt;
    params.require(:comment)&lt;br /&gt;
    Suggestion.find(params[:id])&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Approve a suggestion, even if it was previously rejected.&lt;br /&gt;
  def approve&lt;br /&gt;
    deny_student('Students cannot approve a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Only go through the approval process if the suggestion isn't already approved.&lt;br /&gt;
    unless @suggestion.status == 'Approved'&lt;br /&gt;
      # Since this process updates multiple records at once, wrap them in&lt;br /&gt;
      #   a transaction so that either all of them succeed, or none of them.&lt;br /&gt;
      transaction do&lt;br /&gt;
        # The approval process is:&lt;br /&gt;
        # 1. Mark the suggestion as approved&lt;br /&gt;
        # 2. Create a new topic from the suggestion&lt;br /&gt;
        # 3. Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
        # 4. Send the topic approved message&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        # If a suggestion is anonymous, the suggester is&lt;br /&gt;
        #  unknown and thus steps 3 and 4 can't be taken.&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up!&lt;br /&gt;
          send_notice_of_approval&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # A new Suggestion record is made.&lt;br /&gt;
  def create&lt;br /&gt;
    params.require(%i[anonymous assignment_id auto_signup comment description title])&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      # Anonymous suggestions are allowed by nulling user_id.&lt;br /&gt;
      user_id: params[:anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Delete a suggestion from the records.&lt;br /&gt;
  def destroy&lt;br /&gt;
    deny_student('Students cannot delete suggestions.')&lt;br /&gt;
    Suggestion.find(params[:id]).destroy!&lt;br /&gt;
    render json: {}, status: :no_content # 204&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a list of all Suggestion records associated with a particular assignment.&lt;br /&gt;
  def index&lt;br /&gt;
    deny_student('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
    render json: Suggestion.where(assignment_id: params[:id]), status: :ok # 200&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Reject a suggestion unless it was already approved.&lt;br /&gt;
  def reject&lt;br /&gt;
    deny_student('Students cannot reject a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Since the approval process makes changes to many records,&lt;br /&gt;
    #   rejecting a previously approved suggestion is not possible.&lt;br /&gt;
    if @suggestion.status == 'Approved'&lt;br /&gt;
      render json: { error: 'Suggestion has already been approved.' }, status: :unprocessable_entity # 422&lt;br /&gt;
    elsif @suggestion.status == 'Initialized'&lt;br /&gt;
      # The rejection process is:&lt;br /&gt;
      # 1. Mark the suggestion as rejected&lt;br /&gt;
      # 2. Send the topic rejected message&lt;br /&gt;
      @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      send_notice_of_rejection if @suggestion.user_id&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a single Suggestion record.&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only view their own suggestions.')&lt;br /&gt;
    render json: {&lt;br /&gt;
      comments: SuggestionComment.where(suggestion_id: params[:id]),&lt;br /&gt;
      suggestion: @suggestion&lt;br /&gt;
    }, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Change the details of a Suggestion.&lt;br /&gt;
  def update&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only edit their own suggestions.')&lt;br /&gt;
    # Only title, description, and signup preference can be changed.&lt;br /&gt;
    @suggestion.update!(params.permit(:title, :description, :auto_signup))&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    # Convert a suggestion into a fully fledged topic.&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1,&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_student(err_msg)&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_non_owner_student(err_msg)&lt;br /&gt;
    # If the student owns the resource, they are allowed to perform&lt;br /&gt;
    #   every action related to Suggestions and SuggestionComments.&lt;br /&gt;
    return if @suggestion.user_id == @current_user.id&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval&lt;br /&gt;
    # Email the suggester and CC the suggester's teammates that the suggestion was approved&lt;br /&gt;
    Mailer.send_topic_approved_email(&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      suggester: @suggester,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_rejection&lt;br /&gt;
    # Email the suggester that the suggestion was rejected&lt;br /&gt;
    Mailer.send_topic_rejected_email(&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been rejected&amp;quot;,&lt;br /&gt;
      suggester: User.find(@suggestion.user_id),&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    # Find the suggester's team which is signed up to the topic's assignment.&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    # Create the team if necessary.&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    # If the suggester's team has no assignment, then clear its&lt;br /&gt;
    #   waitlist and sign it up to the newly created topic.&lt;br /&gt;
    unless SignedUpTeam.exists?(team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: true).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    # Since the suggester's team is signed up to the topic, make it private&lt;br /&gt;
    #   to the suggester so no other teams will attempt to sign up to it.&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML.&amp;lt;br&amp;gt;The JSON API is described in [[#JSON API|JSON API]]&lt;br /&gt;
* Switched from check-avoid to fail-recover coding style to minimize number of if statements and thus indentation.&amp;lt;br&amp;gt;As a result, the methods become easier to understand, as the intended behavior is programmed all in one block with each error handled all the way at the end of the method&lt;br /&gt;
* Methods have proper error handling.&amp;lt;br&amp;gt;Each method was given a 'rescue' clause for any error that might occur, and most ActiveRecords methods were switched from the ones that return a boolean to the ones that throw an exception&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects and end in an exclamation point if the method modifies the database&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-segmented into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries.&amp;lt;br&amp;gt;Additionally, a complementary email method for rejections has been addded&lt;br /&gt;
* Where required, API entry points call dedicated privilege check methods&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, cc: defn[:cc], subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_topic_rejected_email(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;authorization_helper.rb (Trimmed)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (authorization_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_approved_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been approved.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you want to work on this topic, you can log into Expertiza and switch topics.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_rejected_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been rejected.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you believe this to be an error, please contact your instructor or one of the TAs.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The authorization helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
The mailer helper is accompanied by its template files for each type of message sent, in this case the topic approved and topic rejected messages. They also exist as plain text template files, since the original repo had both html and plaintext versions of the email templates. The plaintext versions of the template files are not shown as their content is the same, just stripped of the HTML tags.&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the following section:&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  get 'show suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when user is a instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
      response '200', 'suggestion details and comments returned' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
          expect(data['comments']).to be_an(Array)&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is owner of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        # make sure user is set as owner of suggestion&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
        # test for student being able to view their own suggestion&lt;br /&gt;
        response '200', 'student can view their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is not owner of suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
        response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for when suggestion doesn't exist&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
  post 'Add a comment to a suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
    parameter name: :comment, in: :body, schema: {&lt;br /&gt;
      type: :object,&lt;br /&gt;
      properties: {&lt;br /&gt;
        comment: { type: :string }&lt;br /&gt;
      },&lt;br /&gt;
      required: ['comment']&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # successful comment addition test&lt;br /&gt;
    response '201', 'comment_added' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for missing or empty comment&lt;br /&gt;
    response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
      let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        # Mock the params to simulate the request&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
        # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
        allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
      let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
  post 'Approve suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # successful suggestion approval&lt;br /&gt;
      response '200', 'suggestion approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Approved')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable with a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for unprocessable without a record&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when a suggestion is not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user as not having ta_privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for students not being able to approve suggestions&lt;br /&gt;
      response '403', 'students cannot approve suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
  post 'Reject suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is instructor/ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion rejection&lt;br /&gt;
      response '200', 'suggestion rejected' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['status']).to eq('Rejected')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion already being approved&lt;br /&gt;
      response '422', 'suggestion already approved' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulate suggestion status as 'Approved'&lt;br /&gt;
          suggestion.update!(status: 'Approved')&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          parsed_response = JSON.parse(response.body)&lt;br /&gt;
          expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when the suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
      response '403', 'students cannot reject suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  patch 'update suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion update&lt;br /&gt;
      response '200', 'suggestion updated' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to be a student and setup current user&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is owner/a part of suggestion&lt;br /&gt;
      context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student owning the suggestion&lt;br /&gt;
          suggestion.update!(user_id: @user.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students can update their own suggestion(s)&lt;br /&gt;
        response '200', 'student can update their own suggestion' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when student is not owner to suggestion&lt;br /&gt;
      context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          # Simulate the student not owning the suggestion&lt;br /&gt;
          suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
          allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
        response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion not being found&lt;br /&gt;
    response '404', 'suggestion not found' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(404)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for suggestion being invalid in some form/missing&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
        allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  post 'Create suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
              description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
    # test for successful suggestion creation&lt;br /&gt;
    response '201', 'suggestion created successfully' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Suggestion title',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return properly for test&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(201)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
        expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
        expect(data['auto_signup']).to eq(true)&lt;br /&gt;
        expect(data['status']).to eq('Initialized')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for a missing parameter&lt;br /&gt;
    response '422', 'missing title' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: '',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # set up current user to return current user properly&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
        data = JSON.parse(response.body)&lt;br /&gt;
        expect(data['error']).to eq('title is missing')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test for invalid suggestion given&lt;br /&gt;
    response '422', 'unprocessable entity' do&lt;br /&gt;
      let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
      let(:suggestion) do&lt;br /&gt;
        {&lt;br /&gt;
          assignment_id: assignment.id,&lt;br /&gt;
          auto_signup: true,&lt;br /&gt;
          comment: 'This is a great suggestion!',&lt;br /&gt;
          description: 'Detailed suggestion description',&lt;br /&gt;
          title: 'Sample Suggestion',&lt;br /&gt;
          anonymous: false&lt;br /&gt;
        }&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      before do&lt;br /&gt;
        allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
        allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      run_test! do |response|&lt;br /&gt;
        expect(response.status).to eq(422)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  delete 'Delete suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion deletion&lt;br /&gt;
      response '204', 'suggestion deleted' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(204)&lt;br /&gt;
          expect(response.body).to be_empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for record not being destroyed&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the deletion process&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion is not found/doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test to make sure students cannot delete suggestions&lt;br /&gt;
      response '403', 'students cannot delete suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (index test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  get 'list all suggestions' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful indexing&lt;br /&gt;
      response '200', 'suggestions listed' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
         run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
          expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test case for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for student to be forbidden from indexing suggestions&lt;br /&gt;
      response '403', 'students cannot index suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Updated Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def login_user&lt;br /&gt;
  # Create a user using the factory&lt;br /&gt;
  user = create(:user)&lt;br /&gt;
&lt;br /&gt;
  # Make a POST request to login&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
&lt;br /&gt;
  # Parse the JSON response and extract the token&lt;br /&gt;
  json_response = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
  # Return the token from the response&lt;br /&gt;
  { token: json_response['token'], user: }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  # Login user, grab token, set user and current user&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    auth_data = login_user&lt;br /&gt;
    @token = auth_data[:token]&lt;br /&gt;
    @user = auth_data[:user]&lt;br /&gt;
    @current_user = @user&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # create default assignment and suggestion&lt;br /&gt;
  let(:assignment) { create(:assignment) }&lt;br /&gt;
  let(:suggestion) { create(:suggestion, assignment_id: assignment.id, user_id: @user.id) }&lt;br /&gt;
&lt;br /&gt;
  # Testing for add_comment method&lt;br /&gt;
  path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
    post 'Add a comment to a suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
      parameter name: :comment, in: :body, schema: {&lt;br /&gt;
        type: :object,&lt;br /&gt;
        properties: {&lt;br /&gt;
          comment: { type: :string }&lt;br /&gt;
        },&lt;br /&gt;
        required: ['comment']&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      # successful comment addition test&lt;br /&gt;
      response '201', 'comment_added' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for missing or empty comment&lt;br /&gt;
      response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Mock the params to simulate the request&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
          # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
        let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Tests for approving suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
    post 'Approve suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # successful suggestion approval&lt;br /&gt;
        response '200', 'suggestion approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Approved')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable with a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable without a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when a suggestion is not found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user as not having ta_privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for students not being able to approve suggestions&lt;br /&gt;
        response '403', 'students cannot approve suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for creating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    post 'Create suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
                description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion creation&lt;br /&gt;
      response '201', 'suggestion created successfully' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Suggestion title',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return properly for test&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
          expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
          expect(data['auto_signup']).to eq(true)&lt;br /&gt;
          expect(data['status']).to eq('Initialized')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for a missing parameter&lt;br /&gt;
      response '422', 'missing title' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: '',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return current user properly&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['error']).to eq('title is missing')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for invalid suggestion given&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Sample Suggestion',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
          suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
          allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for deleting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    delete 'Delete suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion deletion&lt;br /&gt;
        response '204', 'suggestion deleted' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(204)&lt;br /&gt;
            expect(response.body).to be_empty&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for record not being destroyed&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the deletion process&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when suggestion is not found/doesn't exist&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students cannot delete suggestions&lt;br /&gt;
        response '403', 'students cannot delete suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for indexing/listing all suggestions&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    get 'list all suggestions' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
          suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful indexing&lt;br /&gt;
        response '200', 'suggestions listed' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
            expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for student to be forbidden from indexing suggestions&lt;br /&gt;
        response '403', 'students cannot index suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for rejecting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
    post 'Reject suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is instructor/ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion rejection&lt;br /&gt;
        response '200', 'suggestion rejected' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Rejected')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for suggestion already being approved&lt;br /&gt;
        response '422', 'suggestion already approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulate suggestion status as 'Approved'&lt;br /&gt;
            suggestion.update!(status: 'Approved')&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
            parsed_response = JSON.parse(response.body)&lt;br /&gt;
            expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when the suggestion not being found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
        response '403', 'students cannot reject suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for showing suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    get 'show suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when user is a instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
        response '200', 'suggestion details and comments returned' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when user is owner of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          # make sure user is set as owner of suggestion&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
          # test for student being able to view their own suggestion&lt;br /&gt;
          response '200', 'student can view their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              data = JSON.parse(response.body)&lt;br /&gt;
              expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
              expect(data['comments']).to be_an(Array)&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test case for when user is not owner of suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
          response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for updating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    patch 'update suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion update&lt;br /&gt;
        response '200', 'suggestion updated' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to be a student and setup current user&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is owner/a part of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test to make sure students can update their own suggestion(s)&lt;br /&gt;
          response '200', 'student can update their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is not owner to suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
          response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion being invalid in some form/missing&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
          allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add detail to testing&lt;br /&gt;
* Record testing video&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Pranavi Sanganabhatla (psangan@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
* [# Testing video]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160758</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160758"/>
		<updated>2024-12-04T05:39:13Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Detailed Test Plan */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
The controller is now fully commented, and this wiki page goes more in-depth about the code, design, and testing.&lt;br /&gt;
&lt;br /&gt;
Various other QoL changes are implemented, such as alphabetized hash parameters and collapsible source code views.&lt;br /&gt;
&lt;br /&gt;
The RSpec file is still under construction.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required: (skip steps 3 and 4 if automatic sign up is turned off)&lt;br /&gt;
## Mark the suggestion as approved&lt;br /&gt;
## Create a new topic from the suggestion&lt;br /&gt;
## Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
## Send the topic approved message&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Action &lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Method name&lt;br /&gt;
! Comment&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; | no change &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | add_comment&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve&lt;br /&gt;
| assimilated approve_suggestion&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | show&lt;br /&gt;
| assimilated student_view&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;5&amp;quot; | added &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create_topic_from_suggestion!&lt;br /&gt;
| chain of helper methods distilled into single call to create&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_student&lt;br /&gt;
| privilege check helper method, only TA and higher can pass&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_non_owner_student&lt;br /&gt;
| privilege check helper method, same as above, but student passes if he is the owner&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | destroy&lt;br /&gt;
| it should be possible to delete a suggestion (D in CRUD)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | send_notice_of_rejection&lt;br /&gt;
| complementary message to approval message&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | removed &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | action_allowed?&lt;br /&gt;
| different methods require different authorization checks, use dedicated helpers instead&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve_suggestion&lt;br /&gt;
| merged into approve&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | new&lt;br /&gt;
| view methods don't apply to JSON APIs&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_edit&lt;br /&gt;
| editing a suggestion is not allowed&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_view&lt;br /&gt;
| merged into show&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | submit&lt;br /&gt;
| call-this-or-that method&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | suggestion_params&lt;br /&gt;
| inlined into before_action declaration&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;6&amp;quot; | renamed &lt;br /&gt;
! Old&lt;br /&gt;
! New&lt;br /&gt;
!&lt;br /&gt;
|-&lt;br /&gt;
| list&lt;br /&gt;
| index&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| notification&lt;br /&gt;
| sign_team_up!&lt;br /&gt;
| method name and implementation didn't match&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion&lt;br /&gt;
| reject&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| send_email&lt;br /&gt;
| send_notice_of_approval&lt;br /&gt;
| method names should be specific&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion&lt;br /&gt;
| update&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code. Additionally, the suggestions controller now notifies the suggester if his suggestion has been rejected instead of just silently updating the state.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
This task specifically targeted the view files. Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
Even so, DRY was observed throughout the project by extracting common statements into helper functions, most notably the privilege check methods.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
===Tables===&lt;br /&gt;
&lt;br /&gt;
The following tables are newly added to the database schema. The question mark at the end of foreign_key indicates that this field is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:tables.png]]&lt;br /&gt;
&lt;br /&gt;
===Model associations===&lt;br /&gt;
&lt;br /&gt;
While simple at first glance, the suggestions controller interacts with many models in the system.&lt;br /&gt;
&lt;br /&gt;
The models in the first row of the diagram are the direct subject material of the controller as most API endpoints perform CRUD operations on only these two. The grayed out models in the second row are only ever read by the controller, they can be considered static throughout any call to the controller. The remaining four models in the last row and column of the diagram are handled by the suggestion approval process. The controller may perform any of the CRUD operations on these models.&lt;br /&gt;
&lt;br /&gt;
The arrows in the diagram point in the direction of ownership, that is, an arrow points from models A to B if A belongs to B. The dashed arrow labeled 'optional' in the legend means that the foreign key is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:ActiveRecord Associations.png]]&lt;br /&gt;
&lt;br /&gt;
===Controller and collaborators===&lt;br /&gt;
&lt;br /&gt;
The following diagram shows the controller, its API endpoints, its collaborators, and the methods accessed from the controller. The AuthorizationHelper module is imported into the controller class.&lt;br /&gt;
&lt;br /&gt;
[[File:classes.png]]&lt;br /&gt;
&lt;br /&gt;
==JSON API==&lt;br /&gt;
&lt;br /&gt;
The backend reimplementation project aims to convert Expertiza into a split frontend-backend service. To this end, the backend will respond with JSON only.&lt;br /&gt;
&lt;br /&gt;
===Request Format===&lt;br /&gt;
&lt;br /&gt;
In addition to the authorization headers, a combination of seven fields may be required for the specific API endpoint. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! rowspan=&amp;quot;2&amp;quot; | Parameter !! colspan=&amp;quot;8&amp;quot; | Included in API Endpoint !! rowspan=&amp;quot;2&amp;quot; | Type !! rowspan=&amp;quot;2&amp;quot; | Description&lt;br /&gt;
|-&lt;br /&gt;
! add_comment !! approve !! create !! destroy !! index !! reject !! show !! update&lt;br /&gt;
|-&lt;br /&gt;
! id&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓* || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || integer || Suggestion ID&lt;br /&gt;
|-&lt;br /&gt;
! anonymous&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || boolean || Whether to nullify user ID of suggester&lt;br /&gt;
|-&lt;br /&gt;
! assignment_id&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || integer || Assignment ID&lt;br /&gt;
|-&lt;br /&gt;
! auto_signup&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || boolean || Whether to sign up team when approved&lt;br /&gt;
|-&lt;br /&gt;
! comment&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || string || SuggestionComment content&lt;br /&gt;
|-&lt;br /&gt;
! description&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion description&lt;br /&gt;
|-&lt;br /&gt;
! title&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion title&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
* *: The id parameter specifies the Assignment ID instead&lt;br /&gt;
* ✓: Field required&lt;br /&gt;
* ✕: Field ignored&lt;br /&gt;
* ~: Field optional&lt;br /&gt;
&lt;br /&gt;
For example, the request data for the add_comment method would be { &amp;quot;id&amp;quot;: 1, &amp;quot;comment&amp;quot;: &amp;quot;This is a comment on one of the suggestions&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
===Status Codes===&lt;br /&gt;
&lt;br /&gt;
Depending on the parameters and database records, different HTTP status codes may be returned. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Code || Mnemonic || Condition || API Endpoints&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 2xx || success || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 200 || ok || Request successful || approve&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&amp;lt;br&amp;gt;(c&amp;lt;strong&amp;gt;RU&amp;lt;/strong&amp;gt;d)&lt;br /&gt;
|-&lt;br /&gt;
| 201 || created || Comment successfully created&amp;lt;br&amp;gt;SuggestionComment successfully created || add_comment&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;(&amp;lt;strong&amp;gt;C&amp;lt;/strong&amp;gt;rud)&lt;br /&gt;
|-&lt;br /&gt;
| 204 || no content || Suggestion successfully deleted || delete&amp;lt;br&amp;gt;(cru&amp;lt;strong&amp;gt;D&amp;lt;/strong&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 4xx || client error || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 403 || forbidden || Privilege check fails || approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 404 || not found || ActiveRecord::RecordNotFound exception || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 422 || unprocessable entity || ActiveRecord::RecordInvalid exception&amp;lt;br&amp;gt;ActiveRecord::RecordNotDestroyed exception&amp;lt;br&amp;gt;Suggestion already approved when trying to reject || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;update&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Response Format===&lt;br /&gt;
&lt;br /&gt;
Depending on the API endpoint of the request, a different entity may be returned. Note that only a success (2xx) will return an entity, a client error (4xx) will return an error message instead. The returned entities are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! API Endpoint || Rendered Model&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || SuggestionComment&lt;br /&gt;
|-&lt;br /&gt;
| approve || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| create || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| destroy || { }&lt;br /&gt;
|-&lt;br /&gt;
| index || Array of Suggestions&lt;br /&gt;
|-&lt;br /&gt;
| reject || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| show || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| update || Suggestion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
Each of the boxes with file names are expandable and contain the contents of the file. If applicable, a before-after view is provided.&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :title, :description, presence: true&lt;br /&gt;
  has_many :suggestion_comments&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :assignment&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
# https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  # Strong params inlined as global before_action&lt;br /&gt;
  before_action except: [:index] do&lt;br /&gt;
    params.require(:id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Comment on a suggestion.&lt;br /&gt;
  # A new SuggestionComment record is made.&lt;br /&gt;
  def add_comment&lt;br /&gt;
    params.require(:comment)&lt;br /&gt;
    Suggestion.find(params[:id])&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Approve a suggestion, even if it was previously rejected.&lt;br /&gt;
  def approve&lt;br /&gt;
    deny_student('Students cannot approve a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Only go through the approval process if the suggestion isn't already approved.&lt;br /&gt;
    unless @suggestion.status == 'Approved'&lt;br /&gt;
      # Since this process updates multiple records at once, wrap them in&lt;br /&gt;
      #   a transaction so that either all of them succeed, or none of them.&lt;br /&gt;
      transaction do&lt;br /&gt;
        # The approval process is:&lt;br /&gt;
        # 1. Mark the suggestion as approved&lt;br /&gt;
        # 2. Create a new topic from the suggestion&lt;br /&gt;
        # 3. Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
        # 4. Send the topic approved message&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        # If a suggestion is anonymous, the suggester is&lt;br /&gt;
        #  unknown and thus steps 3 and 4 can't be taken.&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up!&lt;br /&gt;
          send_notice_of_approval&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # A new Suggestion record is made.&lt;br /&gt;
  def create&lt;br /&gt;
    params.require(%i[anonymous assignment_id auto_signup comment description title])&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      # Anonymous suggestions are allowed by nulling user_id.&lt;br /&gt;
      user_id: params[:anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Delete a suggestion from the records.&lt;br /&gt;
  def destroy&lt;br /&gt;
    deny_student('Students cannot delete suggestions.')&lt;br /&gt;
    Suggestion.find(params[:id]).destroy!&lt;br /&gt;
    render json: {}, status: :no_content # 204&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a list of all Suggestion records associated with a particular assignment.&lt;br /&gt;
  def index&lt;br /&gt;
    deny_student('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
    render json: Suggestion.where(assignment_id: params[:id]), status: :ok # 200&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Reject a suggestion unless it was already approved.&lt;br /&gt;
  def reject&lt;br /&gt;
    deny_student('Students cannot reject a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Since the approval process makes changes to many records,&lt;br /&gt;
    #   rejecting a previously approved suggestion is not possible.&lt;br /&gt;
    if @suggestion.status == 'Approved'&lt;br /&gt;
      render json: { error: 'Suggestion has already been approved.' }, status: :unprocessable_entity # 422&lt;br /&gt;
    elsif @suggestion.status == 'Initialized'&lt;br /&gt;
      # The rejection process is:&lt;br /&gt;
      # 1. Mark the suggestion as rejected&lt;br /&gt;
      # 2. Send the topic rejected message&lt;br /&gt;
      @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      send_notice_of_rejection if @suggestion.user_id&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a single Suggestion record.&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only view their own suggestions.')&lt;br /&gt;
    render json: {&lt;br /&gt;
      comments: SuggestionComment.where(suggestion_id: params[:id]),&lt;br /&gt;
      suggestion: @suggestion&lt;br /&gt;
    }, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Change the details of a Suggestion.&lt;br /&gt;
  def update&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only edit their own suggestions.')&lt;br /&gt;
    # Only title, description, and signup preference can be changed.&lt;br /&gt;
    @suggestion.update!(params.permit(:title, :description, :auto_signup))&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    # Convert a suggestion into a fully fledged topic.&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1,&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_student(err_msg)&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_non_owner_student(err_msg)&lt;br /&gt;
    # If the student owns the resource, they are allowed to perform&lt;br /&gt;
    #   every action related to Suggestions and SuggestionComments.&lt;br /&gt;
    return if @suggestion.user_id == @current_user.id&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval&lt;br /&gt;
    # Email the suggester and CC the suggester's teammates that the suggestion was approved&lt;br /&gt;
    Mailer.send_topic_approved_email(&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      suggester: @suggester,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_rejection&lt;br /&gt;
    # Email the suggester that the suggestion was rejected&lt;br /&gt;
    Mailer.send_topic_rejected_email(&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been rejected&amp;quot;,&lt;br /&gt;
      suggester: User.find(@suggestion.user_id),&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    # Find the suggester's team which is signed up to the topic's assignment.&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    # Create the team if necessary.&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    # If the suggester's team has no assignment, then clear its&lt;br /&gt;
    #   waitlist and sign it up to the newly created topic.&lt;br /&gt;
    unless SignedUpTeam.exists?(team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: true).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    # Since the suggester's team is signed up to the topic, make it private&lt;br /&gt;
    #   to the suggester so no other teams will attempt to sign up to it.&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML.&amp;lt;br&amp;gt;The JSON API is described in [[#JSON API|JSON API]]&lt;br /&gt;
* Switched from check-avoid to fail-recover coding style to minimize number of if statements and thus indentation.&amp;lt;br&amp;gt;As a result, the methods become easier to understand, as the intended behavior is programmed all in one block with each error handled all the way at the end of the method&lt;br /&gt;
* Methods have proper error handling.&amp;lt;br&amp;gt;Each method was given a 'rescue' clause for any error that might occur, and most ActiveRecords methods were switched from the ones that return a boolean to the ones that throw an exception&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects and end in an exclamation point if the method modifies the database&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-segmented into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries.&amp;lt;br&amp;gt;Additionally, a complementary email method for rejections has been addded&lt;br /&gt;
* Where required, API entry points call dedicated privilege check methods&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, cc: defn[:cc], subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_topic_rejected_email(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;authorization_helper.rb (Trimmed)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (authorization_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_approved_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been approved.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you want to work on this topic, you can log into Expertiza and switch topics.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_rejected_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been rejected.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you believe this to be an error, please contact your instructor or one of the TAs.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The authorization helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
The mailer helper is accompanied by its template files for each type of message sent, in this case the topic approved and topic rejected messages. They also exist as plain text template files, since the original repo had both html and plaintext versions of the email templates. The plaintext versions of the template files are not shown as their content is the same, just stripped of the HTML tags.&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the following section:&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (destroy test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions/{id}' do&lt;br /&gt;
  delete 'Delete suggestion' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion deletion&lt;br /&gt;
      response '204', 'suggestion deleted' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(204)&lt;br /&gt;
          expect(response.body).to be_empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for record not being destroyed&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Simulating an error in the deletion process&lt;br /&gt;
          allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion is not found/doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test to make sure students cannot delete suggestions&lt;br /&gt;
      response '403', 'students cannot delete suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (index test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  get 'list all suggestions' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful indexing&lt;br /&gt;
      response '200', 'suggestions listed' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
         run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
          expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test case for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for student to be forbidden from indexing suggestions&lt;br /&gt;
      response '403', 'students cannot index suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Updated Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def login_user&lt;br /&gt;
  # Create a user using the factory&lt;br /&gt;
  user = create(:user)&lt;br /&gt;
&lt;br /&gt;
  # Make a POST request to login&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
&lt;br /&gt;
  # Parse the JSON response and extract the token&lt;br /&gt;
  json_response = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
  # Return the token from the response&lt;br /&gt;
  { token: json_response['token'], user: }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  # Login user, grab token, set user and current user&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    auth_data = login_user&lt;br /&gt;
    @token = auth_data[:token]&lt;br /&gt;
    @user = auth_data[:user]&lt;br /&gt;
    @current_user = @user&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # create default assignment and suggestion&lt;br /&gt;
  let(:assignment) { create(:assignment) }&lt;br /&gt;
  let(:suggestion) { create(:suggestion, assignment_id: assignment.id, user_id: @user.id) }&lt;br /&gt;
&lt;br /&gt;
  # Testing for add_comment method&lt;br /&gt;
  path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
    post 'Add a comment to a suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
      parameter name: :comment, in: :body, schema: {&lt;br /&gt;
        type: :object,&lt;br /&gt;
        properties: {&lt;br /&gt;
          comment: { type: :string }&lt;br /&gt;
        },&lt;br /&gt;
        required: ['comment']&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      # successful comment addition test&lt;br /&gt;
      response '201', 'comment_added' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for missing or empty comment&lt;br /&gt;
      response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Mock the params to simulate the request&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
          # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
        let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Tests for approving suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
    post 'Approve suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # successful suggestion approval&lt;br /&gt;
        response '200', 'suggestion approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Approved')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable with a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable without a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when a suggestion is not found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user as not having ta_privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for students not being able to approve suggestions&lt;br /&gt;
        response '403', 'students cannot approve suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for creating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    post 'Create suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
                description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion creation&lt;br /&gt;
      response '201', 'suggestion created successfully' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Suggestion title',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return properly for test&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
          expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
          expect(data['auto_signup']).to eq(true)&lt;br /&gt;
          expect(data['status']).to eq('Initialized')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for a missing parameter&lt;br /&gt;
      response '422', 'missing title' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: '',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return current user properly&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['error']).to eq('title is missing')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for invalid suggestion given&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Sample Suggestion',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
          suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
          allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for deleting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    delete 'Delete suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion deletion&lt;br /&gt;
        response '204', 'suggestion deleted' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(204)&lt;br /&gt;
            expect(response.body).to be_empty&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for record not being destroyed&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the deletion process&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when suggestion is not found/doesn't exist&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students cannot delete suggestions&lt;br /&gt;
        response '403', 'students cannot delete suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for indexing/listing all suggestions&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    get 'list all suggestions' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
          suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful indexing&lt;br /&gt;
        response '200', 'suggestions listed' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
            expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for student to be forbidden from indexing suggestions&lt;br /&gt;
        response '403', 'students cannot index suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for rejecting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
    post 'Reject suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is instructor/ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion rejection&lt;br /&gt;
        response '200', 'suggestion rejected' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Rejected')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for suggestion already being approved&lt;br /&gt;
        response '422', 'suggestion already approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulate suggestion status as 'Approved'&lt;br /&gt;
            suggestion.update!(status: 'Approved')&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
            parsed_response = JSON.parse(response.body)&lt;br /&gt;
            expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when the suggestion not being found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
        response '403', 'students cannot reject suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for showing suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    get 'show suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when user is a instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
        response '200', 'suggestion details and comments returned' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when user is owner of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          # make sure user is set as owner of suggestion&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
          # test for student being able to view their own suggestion&lt;br /&gt;
          response '200', 'student can view their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              data = JSON.parse(response.body)&lt;br /&gt;
              expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
              expect(data['comments']).to be_an(Array)&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test case for when user is not owner of suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
          response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for updating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    patch 'update suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion update&lt;br /&gt;
        response '200', 'suggestion updated' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to be a student and setup current user&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is owner/a part of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test to make sure students can update their own suggestion(s)&lt;br /&gt;
          response '200', 'student can update their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is not owner to suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
          response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion being invalid in some form/missing&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
          allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add detail to testing&lt;br /&gt;
* Record testing video&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Pranavi Sanganabhatla (psangan@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
* [# Testing video]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160749</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160749"/>
		<updated>2024-12-04T05:34:36Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Detailed Test Plan */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
The controller is now fully commented, and this wiki page goes more in-depth about the code, design, and testing.&lt;br /&gt;
&lt;br /&gt;
Various other QoL changes are implemented, such as alphabetized hash parameters and collapsible source code views.&lt;br /&gt;
&lt;br /&gt;
The RSpec file is still under construction.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required: (skip steps 3 and 4 if automatic sign up is turned off)&lt;br /&gt;
## Mark the suggestion as approved&lt;br /&gt;
## Create a new topic from the suggestion&lt;br /&gt;
## Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
## Send the topic approved message&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Action &lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Method name&lt;br /&gt;
! Comment&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; | no change &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | add_comment&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve&lt;br /&gt;
| assimilated approve_suggestion&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | show&lt;br /&gt;
| assimilated student_view&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;5&amp;quot; | added &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create_topic_from_suggestion!&lt;br /&gt;
| chain of helper methods distilled into single call to create&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_student&lt;br /&gt;
| privilege check helper method, only TA and higher can pass&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_non_owner_student&lt;br /&gt;
| privilege check helper method, same as above, but student passes if he is the owner&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | destroy&lt;br /&gt;
| it should be possible to delete a suggestion (D in CRUD)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | send_notice_of_rejection&lt;br /&gt;
| complementary message to approval message&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | removed &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | action_allowed?&lt;br /&gt;
| different methods require different authorization checks, use dedicated helpers instead&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve_suggestion&lt;br /&gt;
| merged into approve&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | new&lt;br /&gt;
| view methods don't apply to JSON APIs&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_edit&lt;br /&gt;
| editing a suggestion is not allowed&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_view&lt;br /&gt;
| merged into show&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | submit&lt;br /&gt;
| call-this-or-that method&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | suggestion_params&lt;br /&gt;
| inlined into before_action declaration&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;6&amp;quot; | renamed &lt;br /&gt;
! Old&lt;br /&gt;
! New&lt;br /&gt;
!&lt;br /&gt;
|-&lt;br /&gt;
| list&lt;br /&gt;
| index&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| notification&lt;br /&gt;
| sign_team_up!&lt;br /&gt;
| method name and implementation didn't match&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion&lt;br /&gt;
| reject&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| send_email&lt;br /&gt;
| send_notice_of_approval&lt;br /&gt;
| method names should be specific&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion&lt;br /&gt;
| update&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code. Additionally, the suggestions controller now notifies the suggester if his suggestion has been rejected instead of just silently updating the state.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
This task specifically targeted the view files. Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
Even so, DRY was observed throughout the project by extracting common statements into helper functions, most notably the privilege check methods.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
===Tables===&lt;br /&gt;
&lt;br /&gt;
The following tables are newly added to the database schema. The question mark at the end of foreign_key indicates that this field is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:tables.png]]&lt;br /&gt;
&lt;br /&gt;
===Model associations===&lt;br /&gt;
&lt;br /&gt;
While simple at first glance, the suggestions controller interacts with many models in the system.&lt;br /&gt;
&lt;br /&gt;
The models in the first row of the diagram are the direct subject material of the controller as most API endpoints perform CRUD operations on only these two. The grayed out models in the second row are only ever read by the controller, they can be considered static throughout any call to the controller. The remaining four models in the last row and column of the diagram are handled by the suggestion approval process. The controller may perform any of the CRUD operations on these models.&lt;br /&gt;
&lt;br /&gt;
The arrows in the diagram point in the direction of ownership, that is, an arrow points from models A to B if A belongs to B. The dashed arrow labeled 'optional' in the legend means that the foreign key is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:ActiveRecord Associations.png]]&lt;br /&gt;
&lt;br /&gt;
===Controller and collaborators===&lt;br /&gt;
&lt;br /&gt;
The following diagram shows the controller, its API endpoints, its collaborators, and the methods accessed from the controller. The AuthorizationHelper module is imported into the controller class.&lt;br /&gt;
&lt;br /&gt;
[[File:classes.png]]&lt;br /&gt;
&lt;br /&gt;
==JSON API==&lt;br /&gt;
&lt;br /&gt;
The backend reimplementation project aims to convert Expertiza into a split frontend-backend service. To this end, the backend will respond with JSON only.&lt;br /&gt;
&lt;br /&gt;
===Request Format===&lt;br /&gt;
&lt;br /&gt;
In addition to the authorization headers, a combination of seven fields may be required for the specific API endpoint. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! rowspan=&amp;quot;2&amp;quot; | Parameter !! colspan=&amp;quot;8&amp;quot; | Included in API Endpoint !! rowspan=&amp;quot;2&amp;quot; | Type !! rowspan=&amp;quot;2&amp;quot; | Description&lt;br /&gt;
|-&lt;br /&gt;
! add_comment !! approve !! create !! destroy !! index !! reject !! show !! update&lt;br /&gt;
|-&lt;br /&gt;
! id&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓* || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || integer || Suggestion ID&lt;br /&gt;
|-&lt;br /&gt;
! anonymous&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || boolean || Whether to nullify user ID of suggester&lt;br /&gt;
|-&lt;br /&gt;
! assignment_id&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || integer || Assignment ID&lt;br /&gt;
|-&lt;br /&gt;
! auto_signup&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || boolean || Whether to sign up team when approved&lt;br /&gt;
|-&lt;br /&gt;
! comment&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || string || SuggestionComment content&lt;br /&gt;
|-&lt;br /&gt;
! description&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion description&lt;br /&gt;
|-&lt;br /&gt;
! title&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion title&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
* *: The id parameter specifies the Assignment ID instead&lt;br /&gt;
* ✓: Field required&lt;br /&gt;
* ✕: Field ignored&lt;br /&gt;
* ~: Field optional&lt;br /&gt;
&lt;br /&gt;
For example, the request data for the add_comment method would be { &amp;quot;id&amp;quot;: 1, &amp;quot;comment&amp;quot;: &amp;quot;This is a comment on one of the suggestions&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
===Status Codes===&lt;br /&gt;
&lt;br /&gt;
Depending on the parameters and database records, different HTTP status codes may be returned. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Code || Mnemonic || Condition || API Endpoints&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 2xx || success || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 200 || ok || Request successful || approve&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&amp;lt;br&amp;gt;(c&amp;lt;strong&amp;gt;RU&amp;lt;/strong&amp;gt;d)&lt;br /&gt;
|-&lt;br /&gt;
| 201 || created || Comment successfully created&amp;lt;br&amp;gt;SuggestionComment successfully created || add_comment&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;(&amp;lt;strong&amp;gt;C&amp;lt;/strong&amp;gt;rud)&lt;br /&gt;
|-&lt;br /&gt;
| 204 || no content || Suggestion successfully deleted || delete&amp;lt;br&amp;gt;(cru&amp;lt;strong&amp;gt;D&amp;lt;/strong&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 4xx || client error || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 403 || forbidden || Privilege check fails || approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 404 || not found || ActiveRecord::RecordNotFound exception || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 422 || unprocessable entity || ActiveRecord::RecordInvalid exception&amp;lt;br&amp;gt;ActiveRecord::RecordNotDestroyed exception&amp;lt;br&amp;gt;Suggestion already approved when trying to reject || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;update&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Response Format===&lt;br /&gt;
&lt;br /&gt;
Depending on the API endpoint of the request, a different entity may be returned. Note that only a success (2xx) will return an entity, a client error (4xx) will return an error message instead. The returned entities are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! API Endpoint || Rendered Model&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || SuggestionComment&lt;br /&gt;
|-&lt;br /&gt;
| approve || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| create || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| destroy || { }&lt;br /&gt;
|-&lt;br /&gt;
| index || Array of Suggestions&lt;br /&gt;
|-&lt;br /&gt;
| reject || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| show || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| update || Suggestion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
Each of the boxes with file names are expandable and contain the contents of the file. If applicable, a before-after view is provided.&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :title, :description, presence: true&lt;br /&gt;
  has_many :suggestion_comments&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :assignment&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
# https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  # Strong params inlined as global before_action&lt;br /&gt;
  before_action except: [:index] do&lt;br /&gt;
    params.require(:id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Comment on a suggestion.&lt;br /&gt;
  # A new SuggestionComment record is made.&lt;br /&gt;
  def add_comment&lt;br /&gt;
    params.require(:comment)&lt;br /&gt;
    Suggestion.find(params[:id])&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Approve a suggestion, even if it was previously rejected.&lt;br /&gt;
  def approve&lt;br /&gt;
    deny_student('Students cannot approve a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Only go through the approval process if the suggestion isn't already approved.&lt;br /&gt;
    unless @suggestion.status == 'Approved'&lt;br /&gt;
      # Since this process updates multiple records at once, wrap them in&lt;br /&gt;
      #   a transaction so that either all of them succeed, or none of them.&lt;br /&gt;
      transaction do&lt;br /&gt;
        # The approval process is:&lt;br /&gt;
        # 1. Mark the suggestion as approved&lt;br /&gt;
        # 2. Create a new topic from the suggestion&lt;br /&gt;
        # 3. Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
        # 4. Send the topic approved message&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        # If a suggestion is anonymous, the suggester is&lt;br /&gt;
        #  unknown and thus steps 3 and 4 can't be taken.&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up!&lt;br /&gt;
          send_notice_of_approval&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # A new Suggestion record is made.&lt;br /&gt;
  def create&lt;br /&gt;
    params.require(%i[anonymous assignment_id auto_signup comment description title])&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      # Anonymous suggestions are allowed by nulling user_id.&lt;br /&gt;
      user_id: params[:anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Delete a suggestion from the records.&lt;br /&gt;
  def destroy&lt;br /&gt;
    deny_student('Students cannot delete suggestions.')&lt;br /&gt;
    Suggestion.find(params[:id]).destroy!&lt;br /&gt;
    render json: {}, status: :no_content # 204&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a list of all Suggestion records associated with a particular assignment.&lt;br /&gt;
  def index&lt;br /&gt;
    deny_student('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
    render json: Suggestion.where(assignment_id: params[:id]), status: :ok # 200&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Reject a suggestion unless it was already approved.&lt;br /&gt;
  def reject&lt;br /&gt;
    deny_student('Students cannot reject a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Since the approval process makes changes to many records,&lt;br /&gt;
    #   rejecting a previously approved suggestion is not possible.&lt;br /&gt;
    if @suggestion.status == 'Approved'&lt;br /&gt;
      render json: { error: 'Suggestion has already been approved.' }, status: :unprocessable_entity # 422&lt;br /&gt;
    elsif @suggestion.status == 'Initialized'&lt;br /&gt;
      # The rejection process is:&lt;br /&gt;
      # 1. Mark the suggestion as rejected&lt;br /&gt;
      # 2. Send the topic rejected message&lt;br /&gt;
      @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      send_notice_of_rejection if @suggestion.user_id&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a single Suggestion record.&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only view their own suggestions.')&lt;br /&gt;
    render json: {&lt;br /&gt;
      comments: SuggestionComment.where(suggestion_id: params[:id]),&lt;br /&gt;
      suggestion: @suggestion&lt;br /&gt;
    }, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Change the details of a Suggestion.&lt;br /&gt;
  def update&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only edit their own suggestions.')&lt;br /&gt;
    # Only title, description, and signup preference can be changed.&lt;br /&gt;
    @suggestion.update!(params.permit(:title, :description, :auto_signup))&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    # Convert a suggestion into a fully fledged topic.&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1,&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_student(err_msg)&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_non_owner_student(err_msg)&lt;br /&gt;
    # If the student owns the resource, they are allowed to perform&lt;br /&gt;
    #   every action related to Suggestions and SuggestionComments.&lt;br /&gt;
    return if @suggestion.user_id == @current_user.id&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval&lt;br /&gt;
    # Email the suggester and CC the suggester's teammates that the suggestion was approved&lt;br /&gt;
    Mailer.send_topic_approved_email(&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      suggester: @suggester,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_rejection&lt;br /&gt;
    # Email the suggester that the suggestion was rejected&lt;br /&gt;
    Mailer.send_topic_rejected_email(&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been rejected&amp;quot;,&lt;br /&gt;
      suggester: User.find(@suggestion.user_id),&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    # Find the suggester's team which is signed up to the topic's assignment.&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    # Create the team if necessary.&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    # If the suggester's team has no assignment, then clear its&lt;br /&gt;
    #   waitlist and sign it up to the newly created topic.&lt;br /&gt;
    unless SignedUpTeam.exists?(team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: true).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    # Since the suggester's team is signed up to the topic, make it private&lt;br /&gt;
    #   to the suggester so no other teams will attempt to sign up to it.&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML.&amp;lt;br&amp;gt;The JSON API is described in [[#JSON API|JSON API]]&lt;br /&gt;
* Switched from check-avoid to fail-recover coding style to minimize number of if statements and thus indentation.&amp;lt;br&amp;gt;As a result, the methods become easier to understand, as the intended behavior is programmed all in one block with each error handled all the way at the end of the method&lt;br /&gt;
* Methods have proper error handling.&amp;lt;br&amp;gt;Each method was given a 'rescue' clause for any error that might occur, and most ActiveRecords methods were switched from the ones that return a boolean to the ones that throw an exception&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects and end in an exclamation point if the method modifies the database&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-segmented into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries.&amp;lt;br&amp;gt;Additionally, a complementary email method for rejections has been addded&lt;br /&gt;
* Where required, API entry points call dedicated privilege check methods&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, cc: defn[:cc], subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_topic_rejected_email(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;authorization_helper.rb (Trimmed)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (authorization_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_approved_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been approved.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you want to work on this topic, you can log into Expertiza and switch topics.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_rejected_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been rejected.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you believe this to be an error, please contact your instructor or one of the TAs.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The authorization helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
The mailer helper is accompanied by its template files for each type of message sent, in this case the topic approved and topic rejected messages. They also exist as plain text template files, since the original repo had both html and plaintext versions of the email templates. The plaintext versions of the template files are not shown as their content is the same, just stripped of the HTML tags.&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the following section:&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (index test)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
path '/api/v1/suggestions' do&lt;br /&gt;
  get 'list all suggestions' do&lt;br /&gt;
    tags 'Suggestions'&lt;br /&gt;
    consumes 'application/json'&lt;br /&gt;
    parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
    parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
    # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
    context '| when user is instructor | ' do&lt;br /&gt;
      # set user to have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for successful indexing&lt;br /&gt;
      response '200', 'suggestions listed' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
         run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(200)&lt;br /&gt;
          expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    # test case for when user is a student&lt;br /&gt;
    context ' | when user is student | ' do&lt;br /&gt;
      # set user to not have ta privileges&lt;br /&gt;
      before(:each) do&lt;br /&gt;
        allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for student to be forbidden from indexing suggestions&lt;br /&gt;
      response '403', 'students cannot index suggestions' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(403)&lt;br /&gt;
          expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Updated Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def login_user&lt;br /&gt;
  # Create a user using the factory&lt;br /&gt;
  user = create(:user)&lt;br /&gt;
&lt;br /&gt;
  # Make a POST request to login&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
&lt;br /&gt;
  # Parse the JSON response and extract the token&lt;br /&gt;
  json_response = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
  # Return the token from the response&lt;br /&gt;
  { token: json_response['token'], user: }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  # Login user, grab token, set user and current user&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    auth_data = login_user&lt;br /&gt;
    @token = auth_data[:token]&lt;br /&gt;
    @user = auth_data[:user]&lt;br /&gt;
    @current_user = @user&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # create default assignment and suggestion&lt;br /&gt;
  let(:assignment) { create(:assignment) }&lt;br /&gt;
  let(:suggestion) { create(:suggestion, assignment_id: assignment.id, user_id: @user.id) }&lt;br /&gt;
&lt;br /&gt;
  # Testing for add_comment method&lt;br /&gt;
  path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
    post 'Add a comment to a suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
      parameter name: :comment, in: :body, schema: {&lt;br /&gt;
        type: :object,&lt;br /&gt;
        properties: {&lt;br /&gt;
          comment: { type: :string }&lt;br /&gt;
        },&lt;br /&gt;
        required: ['comment']&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      # successful comment addition test&lt;br /&gt;
      response '201', 'comment_added' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for missing or empty comment&lt;br /&gt;
      response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Mock the params to simulate the request&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
          # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
        let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Tests for approving suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
    post 'Approve suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # successful suggestion approval&lt;br /&gt;
        response '200', 'suggestion approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Approved')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable with a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable without a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when a suggestion is not found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user as not having ta_privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for students not being able to approve suggestions&lt;br /&gt;
        response '403', 'students cannot approve suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for creating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    post 'Create suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
                description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion creation&lt;br /&gt;
      response '201', 'suggestion created successfully' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Suggestion title',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return properly for test&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
          expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
          expect(data['auto_signup']).to eq(true)&lt;br /&gt;
          expect(data['status']).to eq('Initialized')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for a missing parameter&lt;br /&gt;
      response '422', 'missing title' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: '',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return current user properly&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['error']).to eq('title is missing')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for invalid suggestion given&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Sample Suggestion',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
          suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
          allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for deleting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    delete 'Delete suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion deletion&lt;br /&gt;
        response '204', 'suggestion deleted' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(204)&lt;br /&gt;
            expect(response.body).to be_empty&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for record not being destroyed&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the deletion process&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when suggestion is not found/doesn't exist&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students cannot delete suggestions&lt;br /&gt;
        response '403', 'students cannot delete suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for indexing/listing all suggestions&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    get 'list all suggestions' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
          suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful indexing&lt;br /&gt;
        response '200', 'suggestions listed' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
            expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for student to be forbidden from indexing suggestions&lt;br /&gt;
        response '403', 'students cannot index suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for rejecting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
    post 'Reject suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is instructor/ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion rejection&lt;br /&gt;
        response '200', 'suggestion rejected' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Rejected')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for suggestion already being approved&lt;br /&gt;
        response '422', 'suggestion already approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulate suggestion status as 'Approved'&lt;br /&gt;
            suggestion.update!(status: 'Approved')&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
            parsed_response = JSON.parse(response.body)&lt;br /&gt;
            expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when the suggestion not being found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
        response '403', 'students cannot reject suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for showing suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    get 'show suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when user is a instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
        response '200', 'suggestion details and comments returned' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when user is owner of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          # make sure user is set as owner of suggestion&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
          # test for student being able to view their own suggestion&lt;br /&gt;
          response '200', 'student can view their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              data = JSON.parse(response.body)&lt;br /&gt;
              expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
              expect(data['comments']).to be_an(Array)&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test case for when user is not owner of suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
          response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for updating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    patch 'update suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion update&lt;br /&gt;
        response '200', 'suggestion updated' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to be a student and setup current user&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is owner/a part of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test to make sure students can update their own suggestion(s)&lt;br /&gt;
          response '200', 'student can update their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is not owner to suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
          response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion being invalid in some form/missing&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
          allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add detail to testing&lt;br /&gt;
* Record testing video&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Pranavi Sanganabhatla (psangan@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
* [# Testing video]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160742</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160742"/>
		<updated>2024-12-04T05:31:35Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Updated Spec file */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
The controller is now fully commented, and this wiki page goes more in-depth about the code, design, and testing.&lt;br /&gt;
&lt;br /&gt;
Various other QoL changes are implemented, such as alphabetized hash parameters and collapsible source code views.&lt;br /&gt;
&lt;br /&gt;
The RSpec file is still under construction.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required: (skip steps 3 and 4 if automatic sign up is turned off)&lt;br /&gt;
## Mark the suggestion as approved&lt;br /&gt;
## Create a new topic from the suggestion&lt;br /&gt;
## Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
## Send the topic approved message&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Action &lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Method name&lt;br /&gt;
! Comment&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; | no change &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | add_comment&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve&lt;br /&gt;
| assimilated approve_suggestion&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | show&lt;br /&gt;
| assimilated student_view&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;5&amp;quot; | added &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create_topic_from_suggestion!&lt;br /&gt;
| chain of helper methods distilled into single call to create&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_student&lt;br /&gt;
| privilege check helper method, only TA and higher can pass&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_non_owner_student&lt;br /&gt;
| privilege check helper method, same as above, but student passes if he is the owner&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | destroy&lt;br /&gt;
| it should be possible to delete a suggestion (D in CRUD)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | send_notice_of_rejection&lt;br /&gt;
| complementary message to approval message&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | removed &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | action_allowed?&lt;br /&gt;
| different methods require different authorization checks, use dedicated helpers instead&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve_suggestion&lt;br /&gt;
| merged into approve&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | new&lt;br /&gt;
| view methods don't apply to JSON APIs&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_edit&lt;br /&gt;
| editing a suggestion is not allowed&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_view&lt;br /&gt;
| merged into show&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | submit&lt;br /&gt;
| call-this-or-that method&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | suggestion_params&lt;br /&gt;
| inlined into before_action declaration&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;6&amp;quot; | renamed &lt;br /&gt;
! Old&lt;br /&gt;
! New&lt;br /&gt;
!&lt;br /&gt;
|-&lt;br /&gt;
| list&lt;br /&gt;
| index&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| notification&lt;br /&gt;
| sign_team_up!&lt;br /&gt;
| method name and implementation didn't match&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion&lt;br /&gt;
| reject&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| send_email&lt;br /&gt;
| send_notice_of_approval&lt;br /&gt;
| method names should be specific&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion&lt;br /&gt;
| update&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code. Additionally, the suggestions controller now notifies the suggester if his suggestion has been rejected instead of just silently updating the state.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
This task specifically targeted the view files. Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
Even so, DRY was observed throughout the project by extracting common statements into helper functions, most notably the privilege check methods.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
===Tables===&lt;br /&gt;
&lt;br /&gt;
The following tables are newly added to the database schema. The question mark at the end of foreign_key indicates that this field is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:tables.png]]&lt;br /&gt;
&lt;br /&gt;
===Model associations===&lt;br /&gt;
&lt;br /&gt;
While simple at first glance, the suggestions controller interacts with many models in the system.&lt;br /&gt;
&lt;br /&gt;
The models in the first row of the diagram are the direct subject material of the controller as most API endpoints perform CRUD operations on only these two. The grayed out models in the second row are only ever read by the controller, they can be considered static throughout any call to the controller. The remaining four models in the last row and column of the diagram are handled by the suggestion approval process. The controller may perform any of the CRUD operations on these models.&lt;br /&gt;
&lt;br /&gt;
The arrows in the diagram point in the direction of ownership, that is, an arrow points from models A to B if A belongs to B. The dashed arrow labeled 'optional' in the legend means that the foreign key is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:ActiveRecord Associations.png]]&lt;br /&gt;
&lt;br /&gt;
===Controller and collaborators===&lt;br /&gt;
&lt;br /&gt;
The following diagram shows the controller, its API endpoints, its collaborators, and the methods accessed from the controller. The AuthorizationHelper module is imported into the controller class.&lt;br /&gt;
&lt;br /&gt;
[[File:classes.png]]&lt;br /&gt;
&lt;br /&gt;
==JSON API==&lt;br /&gt;
&lt;br /&gt;
The backend reimplementation project aims to convert Expertiza into a split frontend-backend service. To this end, the backend will respond with JSON only.&lt;br /&gt;
&lt;br /&gt;
===Request Format===&lt;br /&gt;
&lt;br /&gt;
In addition to the authorization headers, a combination of seven fields may be required for the specific API endpoint. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! rowspan=&amp;quot;2&amp;quot; | Parameter !! colspan=&amp;quot;8&amp;quot; | Included in API Endpoint !! rowspan=&amp;quot;2&amp;quot; | Type !! rowspan=&amp;quot;2&amp;quot; | Description&lt;br /&gt;
|-&lt;br /&gt;
! add_comment !! approve !! create !! destroy !! index !! reject !! show !! update&lt;br /&gt;
|-&lt;br /&gt;
! id&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓* || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || integer || Suggestion ID&lt;br /&gt;
|-&lt;br /&gt;
! anonymous&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || boolean || Whether to nullify user ID of suggester&lt;br /&gt;
|-&lt;br /&gt;
! assignment_id&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || integer || Assignment ID&lt;br /&gt;
|-&lt;br /&gt;
! auto_signup&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || boolean || Whether to sign up team when approved&lt;br /&gt;
|-&lt;br /&gt;
! comment&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || string || SuggestionComment content&lt;br /&gt;
|-&lt;br /&gt;
! description&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion description&lt;br /&gt;
|-&lt;br /&gt;
! title&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion title&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
* *: The id parameter specifies the Assignment ID instead&lt;br /&gt;
* ✓: Field required&lt;br /&gt;
* ✕: Field ignored&lt;br /&gt;
* ~: Field optional&lt;br /&gt;
&lt;br /&gt;
For example, the request data for the add_comment method would be { &amp;quot;id&amp;quot;: 1, &amp;quot;comment&amp;quot;: &amp;quot;This is a comment on one of the suggestions&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
===Status Codes===&lt;br /&gt;
&lt;br /&gt;
Depending on the parameters and database records, different HTTP status codes may be returned. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Code || Mnemonic || Condition || API Endpoints&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 2xx || success || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 200 || ok || Request successful || approve&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&amp;lt;br&amp;gt;(c&amp;lt;strong&amp;gt;RU&amp;lt;/strong&amp;gt;d)&lt;br /&gt;
|-&lt;br /&gt;
| 201 || created || Comment successfully created&amp;lt;br&amp;gt;SuggestionComment successfully created || add_comment&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;(&amp;lt;strong&amp;gt;C&amp;lt;/strong&amp;gt;rud)&lt;br /&gt;
|-&lt;br /&gt;
| 204 || no content || Suggestion successfully deleted || delete&amp;lt;br&amp;gt;(cru&amp;lt;strong&amp;gt;D&amp;lt;/strong&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 4xx || client error || ||&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;3&amp;quot; | || 403 || forbidden || Privilege check fails || approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 404 || not found || ActiveRecord::RecordNotFound exception || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| 422 || unprocessable entity || ActiveRecord::RecordInvalid exception&amp;lt;br&amp;gt;ActiveRecord::RecordNotDestroyed exception&amp;lt;br&amp;gt;Suggestion already approved when trying to reject || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;update&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Response Format===&lt;br /&gt;
&lt;br /&gt;
Depending on the API endpoint of the request, a different entity may be returned. Note that only a success (2xx) will return an entity, a client error (4xx) will return an error message instead. The returned entities are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! API Endpoint || Rendered Model&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || SuggestionComment&lt;br /&gt;
|-&lt;br /&gt;
| approve || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| create || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| destroy || { }&lt;br /&gt;
|-&lt;br /&gt;
| index || Array of Suggestions&lt;br /&gt;
|-&lt;br /&gt;
| reject || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| show || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| update || Suggestion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
Each of the boxes with file names are expandable and contain the contents of the file. If applicable, a before-after view is provided.&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :title, :description, presence: true&lt;br /&gt;
  has_many :suggestion_comments&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :assignment&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
# https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  # Strong params inlined as global before_action&lt;br /&gt;
  before_action except: [:index] do&lt;br /&gt;
    params.require(:id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Comment on a suggestion.&lt;br /&gt;
  # A new SuggestionComment record is made.&lt;br /&gt;
  def add_comment&lt;br /&gt;
    params.require(:comment)&lt;br /&gt;
    Suggestion.find(params[:id])&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Approve a suggestion, even if it was previously rejected.&lt;br /&gt;
  def approve&lt;br /&gt;
    deny_student('Students cannot approve a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Only go through the approval process if the suggestion isn't already approved.&lt;br /&gt;
    unless @suggestion.status == 'Approved'&lt;br /&gt;
      # Since this process updates multiple records at once, wrap them in&lt;br /&gt;
      #   a transaction so that either all of them succeed, or none of them.&lt;br /&gt;
      transaction do&lt;br /&gt;
        # The approval process is:&lt;br /&gt;
        # 1. Mark the suggestion as approved&lt;br /&gt;
        # 2. Create a new topic from the suggestion&lt;br /&gt;
        # 3. Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
        # 4. Send the topic approved message&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        # If a suggestion is anonymous, the suggester is&lt;br /&gt;
        #  unknown and thus steps 3 and 4 can't be taken.&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up!&lt;br /&gt;
          send_notice_of_approval&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # A new Suggestion record is made.&lt;br /&gt;
  def create&lt;br /&gt;
    params.require(%i[anonymous assignment_id auto_signup comment description title])&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      # Anonymous suggestions are allowed by nulling user_id.&lt;br /&gt;
      user_id: params[:anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Delete a suggestion from the records.&lt;br /&gt;
  def destroy&lt;br /&gt;
    deny_student('Students cannot delete suggestions.')&lt;br /&gt;
    Suggestion.find(params[:id]).destroy!&lt;br /&gt;
    render json: {}, status: :no_content # 204&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a list of all Suggestion records associated with a particular assignment.&lt;br /&gt;
  def index&lt;br /&gt;
    deny_student('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
    render json: Suggestion.where(assignment_id: params[:id]), status: :ok # 200&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Reject a suggestion unless it was already approved.&lt;br /&gt;
  def reject&lt;br /&gt;
    deny_student('Students cannot reject a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Since the approval process makes changes to many records,&lt;br /&gt;
    #   rejecting a previously approved suggestion is not possible.&lt;br /&gt;
    if @suggestion.status == 'Approved'&lt;br /&gt;
      render json: { error: 'Suggestion has already been approved.' }, status: :unprocessable_entity # 422&lt;br /&gt;
    elsif @suggestion.status == 'Initialized'&lt;br /&gt;
      # The rejection process is:&lt;br /&gt;
      # 1. Mark the suggestion as rejected&lt;br /&gt;
      # 2. Send the topic rejected message&lt;br /&gt;
      @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      send_notice_of_rejection if @suggestion.user_id&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a single Suggestion record.&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only view their own suggestions.')&lt;br /&gt;
    render json: {&lt;br /&gt;
      comments: SuggestionComment.where(suggestion_id: params[:id]),&lt;br /&gt;
      suggestion: @suggestion&lt;br /&gt;
    }, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Change the details of a Suggestion.&lt;br /&gt;
  def update&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only edit their own suggestions.')&lt;br /&gt;
    # Only title, description, and signup preference can be changed.&lt;br /&gt;
    @suggestion.update!(params.permit(:title, :description, :auto_signup))&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    # Convert a suggestion into a fully fledged topic.&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1,&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_student(err_msg)&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_non_owner_student(err_msg)&lt;br /&gt;
    # If the student owns the resource, they are allowed to perform&lt;br /&gt;
    #   every action related to Suggestions and SuggestionComments.&lt;br /&gt;
    return if @suggestion.user_id == @current_user.id&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval&lt;br /&gt;
    # Email the suggester and CC the suggester's teammates that the suggestion was approved&lt;br /&gt;
    Mailer.send_topic_approved_email(&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      suggester: @suggester,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_rejection&lt;br /&gt;
    # Email the suggester that the suggestion was rejected&lt;br /&gt;
    Mailer.send_topic_rejected_email(&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been rejected&amp;quot;,&lt;br /&gt;
      suggester: User.find(@suggestion.user_id),&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    # Find the suggester's team which is signed up to the topic's assignment.&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    # Create the team if necessary.&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    # If the suggester's team has no assignment, then clear its&lt;br /&gt;
    #   waitlist and sign it up to the newly created topic.&lt;br /&gt;
    unless SignedUpTeam.exists?(team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: true).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    # Since the suggester's team is signed up to the topic, make it private&lt;br /&gt;
    #   to the suggester so no other teams will attempt to sign up to it.&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML.&amp;lt;br&amp;gt;The JSON API is described in [[#JSON API|JSON API]]&lt;br /&gt;
* Switched from check-avoid to fail-recover coding style to minimize number of if statements and thus indentation.&amp;lt;br&amp;gt;As a result, the methods become easier to understand, as the intended behavior is programmed all in one block with each error handled all the way at the end of the method&lt;br /&gt;
* Methods have proper error handling.&amp;lt;br&amp;gt;Each method was given a 'rescue' clause for any error that might occur, and most ActiveRecords methods were switched from the ones that return a boolean to the ones that throw an exception&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects and end in an exclamation point if the method modifies the database&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-segmented into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries.&amp;lt;br&amp;gt;Additionally, a complementary email method for rejections has been addded&lt;br /&gt;
* Where required, API entry points call dedicated privilege check methods&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, cc: defn[:cc], subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_topic_rejected_email(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;authorization_helper.rb (Trimmed)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (authorization_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_approved_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been approved.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you want to work on this topic, you can log into Expertiza and switch topics.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;send_topic_rejected_email.html.erb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot; line&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  Hi,&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  The topic &amp;lt;b&amp;gt;&amp;lt;%= @topic_name %&amp;gt;&amp;lt;/b&amp;gt; suggested by your teammate &amp;lt;b&amp;gt;&amp;lt;%= @suggester %&amp;gt;&amp;lt;/b&amp;gt; has just been rejected.&amp;lt;br&amp;gt;&lt;br /&gt;
  If you believe this to be an error, please contact your instructor or one of the TAs.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
  Thanks&lt;br /&gt;
  &amp;lt;hr&amp;gt;&lt;br /&gt;
  This message has been generated by &amp;lt;a href=&amp;quot;http://expertiza.ncsu.edu&amp;quot;&amp;gt;Expertiza&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The authorization helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
The mailer helper is accompanied by its template files for each type of message sent, in this case the topic approved and topic rejected messages. They also exist as plain text template files, since the original repo had both html and plaintext versions of the email templates. The plaintext versions of the template files are not shown as their content is the same, just stripped of the HTML tags.&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the following section:&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&lt;br /&gt;
=== Updated Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def login_user&lt;br /&gt;
  # Create a user using the factory&lt;br /&gt;
  user = create(:user)&lt;br /&gt;
&lt;br /&gt;
  # Make a POST request to login&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
&lt;br /&gt;
  # Parse the JSON response and extract the token&lt;br /&gt;
  json_response = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
  # Return the token from the response&lt;br /&gt;
  { token: json_response['token'], user: }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  # Login user, grab token, set user and current user&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    auth_data = login_user&lt;br /&gt;
    @token = auth_data[:token]&lt;br /&gt;
    @user = auth_data[:user]&lt;br /&gt;
    @current_user = @user&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # create default assignment and suggestion&lt;br /&gt;
  let(:assignment) { create(:assignment) }&lt;br /&gt;
  let(:suggestion) { create(:suggestion, assignment_id: assignment.id, user_id: @user.id) }&lt;br /&gt;
&lt;br /&gt;
  # Testing for add_comment method&lt;br /&gt;
  path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
    post 'Add a comment to a suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
      parameter name: :comment, in: :body, schema: {&lt;br /&gt;
        type: :object,&lt;br /&gt;
        properties: {&lt;br /&gt;
          comment: { type: :string }&lt;br /&gt;
        },&lt;br /&gt;
        required: ['comment']&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      # successful comment addition test&lt;br /&gt;
      response '201', 'comment_added' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for missing or empty comment&lt;br /&gt;
      response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Mock the params to simulate the request&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
          # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
        let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Tests for approving suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
    post 'Approve suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # successful suggestion approval&lt;br /&gt;
        response '200', 'suggestion approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Approved')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable with a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable without a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when a suggestion is not found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user as not having ta_privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for students not being able to approve suggestions&lt;br /&gt;
        response '403', 'students cannot approve suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for creating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    post 'Create suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
                description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion creation&lt;br /&gt;
      response '201', 'suggestion created successfully' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Suggestion title',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return properly for test&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
          expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
          expect(data['auto_signup']).to eq(true)&lt;br /&gt;
          expect(data['status']).to eq('Initialized')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for a missing parameter&lt;br /&gt;
      response '422', 'missing title' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: '',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return current user properly&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['error']).to eq('title is missing')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for invalid suggestion given&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Sample Suggestion',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
          suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
          allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for deleting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    delete 'Delete suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion deletion&lt;br /&gt;
        response '204', 'suggestion deleted' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(204)&lt;br /&gt;
            expect(response.body).to be_empty&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for record not being destroyed&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the deletion process&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when suggestion is not found/doesn't exist&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students cannot delete suggestions&lt;br /&gt;
        response '403', 'students cannot delete suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for indexing/listing all suggestions&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    get 'list all suggestions' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :query, type: :integer, required: true, description: 'ID of the assignment'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
          suggestion.update(assignment_id: assignment.id)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful indexing&lt;br /&gt;
        response '200', 'suggestions listed' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
            expect(JSON.parse(response.body).size).to eq(1)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for student to be forbidden from indexing suggestions&lt;br /&gt;
        response '403', 'students cannot index suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { assignment.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for rejecting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
    post 'Reject suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is instructor/ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion rejection&lt;br /&gt;
        response '200', 'suggestion rejected' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Rejected')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for suggestion already being approved&lt;br /&gt;
        response '422', 'suggestion already approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulate suggestion status as 'Approved'&lt;br /&gt;
            suggestion.update!(status: 'Approved')&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
            parsed_response = JSON.parse(response.body)&lt;br /&gt;
            expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when the suggestion not being found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
        response '403', 'students cannot reject suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for showing suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    get 'show suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when user is a instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
        response '200', 'suggestion details and comments returned' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when user is owner of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          # make sure user is set as owner of suggestion&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
          # test for student being able to view their own suggestion&lt;br /&gt;
          response '200', 'student can view their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              data = JSON.parse(response.body)&lt;br /&gt;
              expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
              expect(data['comments']).to be_an(Array)&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test case for when user is not owner of suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
          response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for updating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    patch 'update suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion update&lt;br /&gt;
        response '200', 'suggestion updated' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to be a student and setup current user&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is owner/a part of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test to make sure students can update their own suggestion(s)&lt;br /&gt;
          response '200', 'student can update their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is not owner to suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
          response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion being invalid in some form/missing&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
          allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add detail to testing&lt;br /&gt;
* Record testing video&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Pranavi Sanganabhatla (psangan@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
* [# Testing video]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160607</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=160607"/>
		<updated>2024-12-04T04:00:16Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Current Spec file */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
The controller is now fully commented, and this wiki page goes more in-depth about the code, design, and testing.&lt;br /&gt;
&lt;br /&gt;
Various other QoL changes are implemented, such as alphabetized hash parameters and collapsible source code views.&lt;br /&gt;
&lt;br /&gt;
The RSpec file is still under construction.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required: (skip steps 3 and 4 if automatic sign up is turned off)&lt;br /&gt;
## Mark the suggestion as approved&lt;br /&gt;
## Create a new topic from the suggestion&lt;br /&gt;
## Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
## Send the topic approved message&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Action &lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Method name&lt;br /&gt;
! Comment&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;4&amp;quot; | no change &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | add_comment&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve&lt;br /&gt;
| assimilated approve_suggestion&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create&lt;br /&gt;
| compacted logic&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | show&lt;br /&gt;
| assimilated student_view&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;5&amp;quot; | added &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | create_topic_from_suggestion!&lt;br /&gt;
| chain of helper methods distilled into single call to create&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_student&lt;br /&gt;
| privilege check helper method, only TA and higher can pass&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | deny_non_owner_student&lt;br /&gt;
| privilege check helper method, same as above, but student passes if he is the owner&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | destroy&lt;br /&gt;
| it should be possible to delete a suggestion (D in CRUD)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | send_notice_of_rejection&lt;br /&gt;
| complementary message to approval message&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;7&amp;quot; | removed &lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | action_allowed?&lt;br /&gt;
| different methods require different authorization checks, use dedicated helpers instead&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | approve_suggestion&lt;br /&gt;
| merged into approve&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | new&lt;br /&gt;
| view methods don't apply to JSON APIs&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_edit&lt;br /&gt;
| editing a suggestion is not allowed&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | student_view&lt;br /&gt;
| merged into show&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | submit&lt;br /&gt;
| call-this-or-that method&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | suggestion_params&lt;br /&gt;
| inlined into before_action declaration&lt;br /&gt;
|-&lt;br /&gt;
| rowspan=&amp;quot;6&amp;quot; | renamed &lt;br /&gt;
! Old&lt;br /&gt;
! New&lt;br /&gt;
!&lt;br /&gt;
|-&lt;br /&gt;
| list&lt;br /&gt;
| index&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| notification&lt;br /&gt;
| sign_team_up!&lt;br /&gt;
| method name and implementation didn't match&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion&lt;br /&gt;
| reject&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|-&lt;br /&gt;
| send_email&lt;br /&gt;
| send_notice_of_approval&lt;br /&gt;
| method names should be specific&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion&lt;br /&gt;
| update&lt;br /&gt;
| standard Rails controller method naming&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code. Additionally, the suggestions controller now notifies the suggester if his suggestion has been rejected instead of just silently updating the state.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
This task specifically targeted the view files. Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
Even so, DRY was observed throughout the project by extracting common statements into helper functions, most notably the privilege check methods.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
===Tables===&lt;br /&gt;
&lt;br /&gt;
The following tables are newly added to the database schema. The question mark at the end of foreign_key indicates that this field is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:tables.png]]&lt;br /&gt;
&lt;br /&gt;
===Model associations===&lt;br /&gt;
&lt;br /&gt;
While simple at first glance, the suggestions controller interacts with many models in the system.&lt;br /&gt;
&lt;br /&gt;
The models in the first row of the diagram are the direct subject material of the controller as most API endpoints perform CRUD operations on only these two. The grayed out models in the second row are only ever read by the controller, they can be considered static throughout any call to the controller. The remaining four models in the last row and column of the diagram are handled by the suggestion approval process. The controller may perform any of the CRUD operations on these models.&lt;br /&gt;
&lt;br /&gt;
The arrows in the diagram point in the direction of ownership, that is, an arrow points from models A to B if A belongs to B. The dashed arrow labeled 'optional' in the legend means that the foreign key is nullable.&lt;br /&gt;
&lt;br /&gt;
[[File:ActiveRecord Associations.png]]&lt;br /&gt;
&lt;br /&gt;
===Controller and collaborators===&lt;br /&gt;
&lt;br /&gt;
The following diagram shows the controller, its API endpoints, its collaborators, and the methods accessed from the controller. The AuthorizationHelper module is imported into the controller class.&lt;br /&gt;
&lt;br /&gt;
[[File:classes.png]]&lt;br /&gt;
&lt;br /&gt;
==JSON API==&lt;br /&gt;
&lt;br /&gt;
The backend reimplementation project aims to convert Expertiza into a split frontend-backend service. To this end, the backend will respond with JSON only.&lt;br /&gt;
&lt;br /&gt;
===Request Format===&lt;br /&gt;
&lt;br /&gt;
In addition to the authorization headers, a combination of seven fields may be required for the specific API endpoint. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! rowspan=&amp;quot;2&amp;quot; | Parameter !! colspan=&amp;quot;8&amp;quot; | Included in API Endpoint !! rowspan=&amp;quot;2&amp;quot; | Type !! rowspan=&amp;quot;2&amp;quot; | Description&lt;br /&gt;
|-&lt;br /&gt;
! add_comment !! approve !! create !! destroy !! index !! reject !! show !! update&lt;br /&gt;
|-&lt;br /&gt;
! id&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓* || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: green;&amp;quot; | ✓ || integer || Suggestion ID&lt;br /&gt;
|-&lt;br /&gt;
! anonymous&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || boolean || Whether to nullify user ID of suggester&lt;br /&gt;
|-&lt;br /&gt;
! assignment_id&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || integer || Assignment ID&lt;br /&gt;
|-&lt;br /&gt;
! auto_signup&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || boolean || Whether to sign up team when approved&lt;br /&gt;
|-&lt;br /&gt;
! comment&lt;br /&gt;
| style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || string || SuggestionComment content&lt;br /&gt;
|-&lt;br /&gt;
! description&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion description&lt;br /&gt;
|-&lt;br /&gt;
! title&lt;br /&gt;
| style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: green;&amp;quot; | ✓ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: red;&amp;quot; | ✕ || style=&amp;quot;color: orange;&amp;quot; | ~ || string || Suggestion title&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
* *: The id parameter specifies the Assignment ID instead&lt;br /&gt;
* ✓: Field required&lt;br /&gt;
* ✕: Field ignored&lt;br /&gt;
* ~: Field optional&lt;br /&gt;
&lt;br /&gt;
===Status Codes===&lt;br /&gt;
&lt;br /&gt;
Depending on the parameters and database records, different HTTP status codes may be returned. They are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; | Code || Mnemonic || Condition || API Endpoints&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 2xx || success || ||&lt;br /&gt;
|-&lt;br /&gt;
| || 200 || ok || Request successful || approve&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&amp;lt;br&amp;gt;(c&amp;lt;strong&amp;gt;RU&amp;lt;/strong&amp;gt;d)&lt;br /&gt;
|-&lt;br /&gt;
| || 201 || created || Comment successfully created&amp;lt;br&amp;gt;SuggestionComment successfully created || add_comment&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;(&amp;lt;strong&amp;gt;C&amp;lt;/strong&amp;gt;rud)&lt;br /&gt;
|-&lt;br /&gt;
| || 204 || no content || Suggestion successfully deleted || delete&amp;lt;br&amp;gt;(cru&amp;lt;strong&amp;gt;D&amp;lt;/strong&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| colspan=&amp;quot;2&amp;quot; | 4xx || client error || ||&lt;br /&gt;
|-&lt;br /&gt;
| || 403 || forbidden || Privilege check fails || approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;index&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| || 404 || not found || ActiveRecord::RecordNotFound exception || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;show&amp;lt;br&amp;gt;update&lt;br /&gt;
|-&lt;br /&gt;
| || 422 || unprocessable entity || ActiveRecord::RecordInvalid exception&amp;lt;br&amp;gt;ActiveRecord::RecordNotDestroyed exception&amp;lt;br&amp;gt;Suggestion already approved when trying to reject || add_comment&amp;lt;br&amp;gt;approve&amp;lt;br&amp;gt;create&amp;lt;br&amp;gt;destroy&amp;lt;br&amp;gt;reject&amp;lt;br&amp;gt;update&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Response Format===&lt;br /&gt;
&lt;br /&gt;
Depending on the API endpoint of the request, a different entity may be returned. Note that only a success (2xx) will return an entity, a client error (4xx) will return an error message instead. The returned entities are:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! API Endpoint || Rendered Model&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || SuggestionComment&lt;br /&gt;
|-&lt;br /&gt;
| approve || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| create || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| destroy || { }&lt;br /&gt;
|-&lt;br /&gt;
| index || Array of Suggestions&lt;br /&gt;
|-&lt;br /&gt;
| reject || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| show || Suggestion&lt;br /&gt;
|-&lt;br /&gt;
| update || Suggestion&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :title, :description, presence: true&lt;br /&gt;
  has_many :suggestion_comments&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :assignment&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
# https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  # Strong params inlined as global before_action&lt;br /&gt;
  before_action except: [:index] do&lt;br /&gt;
    params.require(:id)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Comment on a suggestion.&lt;br /&gt;
  # A new SuggestionComment record is made.&lt;br /&gt;
  def add_comment&lt;br /&gt;
    params.require(:comment)&lt;br /&gt;
    Suggestion.find(params[:id])&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Approve a suggestion, even if it was previously rejected.&lt;br /&gt;
  def approve&lt;br /&gt;
    deny_student('Students cannot approve a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Only go through the approval process if the suggestion isn't already approved.&lt;br /&gt;
    unless @suggestion.status == 'Approved'&lt;br /&gt;
      # Since this process updates multiple records at once, wrap them in&lt;br /&gt;
      #   a transaction so that either all of them succeed, or none of them.&lt;br /&gt;
      transaction do&lt;br /&gt;
        # The approval process is:&lt;br /&gt;
        # 1. Mark the suggestion as approved&lt;br /&gt;
        # 2. Create a new topic from the suggestion&lt;br /&gt;
        # 3. Sign the suggester's team up (create new team if necessary)&lt;br /&gt;
        # 4. Send the topic approved message&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        # If a suggestion is anonymous, the suggester is&lt;br /&gt;
        #  unknown and thus steps 3 and 4 can't be taken.&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up!&lt;br /&gt;
          send_notice_of_approval&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # A new Suggestion record is made.&lt;br /&gt;
  def create&lt;br /&gt;
    params.require(%i[anonymous assignment_id auto_signup comment description title])&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      # Anonymous suggestions are allowed by nulling user_id.&lt;br /&gt;
      user_id: params[:anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :created # 201&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Delete a suggestion from the records.&lt;br /&gt;
  def destroy&lt;br /&gt;
    deny_student('Students cannot delete suggestions.')&lt;br /&gt;
    Suggestion.find(params[:id]).destroy!&lt;br /&gt;
    render json: {}, status: :no_content # 204&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a list of all Suggestion records associated with a particular assignment.&lt;br /&gt;
  def index&lt;br /&gt;
    deny_student('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
    render json: Suggestion.where(assignment_id: params[:id]), status: :ok # 200&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Reject a suggestion unless it was already approved.&lt;br /&gt;
  def reject&lt;br /&gt;
    deny_student('Students cannot reject a suggestion.')&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    # Since the approval process makes changes to many records,&lt;br /&gt;
    #   rejecting a previously approved suggestion is not possible.&lt;br /&gt;
    if @suggestion.status == 'Approved'&lt;br /&gt;
      render json: { error: 'Suggestion has already been approved.' }, status: :unprocessable_entity # 422&lt;br /&gt;
    elsif @suggestion.status == 'Initialized'&lt;br /&gt;
      # The rejection process is:&lt;br /&gt;
      # 1. Mark the suggestion as rejected&lt;br /&gt;
      # 2. Send the topic rejected message&lt;br /&gt;
      @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      send_notice_of_rejection if @suggestion.user_id&lt;br /&gt;
    end&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Get a single Suggestion record.&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only view their own suggestions.')&lt;br /&gt;
    render json: {&lt;br /&gt;
      comments: SuggestionComment.where(suggestion_id: params[:id]),&lt;br /&gt;
      suggestion: @suggestion&lt;br /&gt;
    }, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Change the details of a Suggestion.&lt;br /&gt;
  def update&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    deny_non_owner_student('Students can only edit their own suggestions.')&lt;br /&gt;
    # Only title, description, and signup preference can be changed.&lt;br /&gt;
    @suggestion.update!(params.permit(:title, :description, :auto_signup))&lt;br /&gt;
    render json: @suggestion, status: :ok # 200&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found # 404&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity # 422&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    # Convert a suggestion into a fully fledged topic.&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1,&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_student(err_msg)&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def deny_non_owner_student(err_msg)&lt;br /&gt;
    # If the student owns the resource, they are allowed to perform&lt;br /&gt;
    #   every action related to Suggestions and SuggestionComments.&lt;br /&gt;
    return if @suggestion.user_id == @current_user.id&lt;br /&gt;
    # TAs and above are allowed to perform every action on every Suggestion and SuggestionComment.&lt;br /&gt;
    return if AuthorizationHelper.current_user_has_ta_privileges?&lt;br /&gt;
&lt;br /&gt;
    # A student account is forbidden access and instead sent an error message.&lt;br /&gt;
    render json: { error: err_msg }, status: :forbidden # 403&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval&lt;br /&gt;
    # Email the suggester and CC the suggester's teammates that the suggestion was approved&lt;br /&gt;
    Mailer.send_topic_approved_email(&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      suggester: @suggester,&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_rejection&lt;br /&gt;
    # Email the suggester that the suggestion was rejected&lt;br /&gt;
    Mailer.send_topic_rejected_email(&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been rejected&amp;quot;,&lt;br /&gt;
      suggester: User.find(@suggestion.user_id),&lt;br /&gt;
      topic_name: @suggestion.title&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    # Find the suggester's team which is signed up to the topic's assignment.&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    # Create the team if necessary.&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    # If the suggester's team has no assignment, then clear its&lt;br /&gt;
    #   waitlist and sign it up to the newly created topic.&lt;br /&gt;
    unless SignedUpTeam.exists?(team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: true).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    # Since the suggester's team is signed up to the topic, make it private&lt;br /&gt;
    #   to the suggester so no other teams will attempt to sign up to it.&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML.&amp;lt;br&amp;gt;The JSON API is described in [[#JSON API|JSON API]]&lt;br /&gt;
* Switched from check-avoid to fail-recover coding style to minimize number of if statements and thus indentation.&amp;lt;br&amp;gt;As a result, the methods become easier to understand, as the intended behavior is programmed all in one block with each error handled all the way at the end of the method&lt;br /&gt;
* Methods have proper error handling.&amp;lt;br&amp;gt;Each method was given a 'rescue' clause for any error that might occur, and most ActiveRecords methods were switched from the ones that return a boolean to the ones that throw an exception&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects and end in an exclamation point if the method modifies the database&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-segmented into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries.&amp;lt;br&amp;gt;Additionally, a complementary email method for rejections has been addded&lt;br /&gt;
* Where required, API entry points call dedicated privilege check methods&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, cc: defn[:cc], subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_topic_rejected_email(defn)&lt;br /&gt;
    @suggester = defn[:suggester]&lt;br /&gt;
    @topic_name = defn[:topic_name]&lt;br /&gt;
    mail(to: @suggester.email, subject: defn[:subject]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;authorization_helper.rb (Trimmed)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (authorization_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The privilege helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the following section:&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&lt;br /&gt;
=== Updated Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def login_user&lt;br /&gt;
  # Create a user using the factory&lt;br /&gt;
  user = create(:user)&lt;br /&gt;
&lt;br /&gt;
  # Make a POST request to login&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
&lt;br /&gt;
  # Parse the JSON response and extract the token&lt;br /&gt;
  json_response = JSON.parse(response.body)&lt;br /&gt;
&lt;br /&gt;
  # Return the token from the response&lt;br /&gt;
  { token: json_response['token'], user: }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  # Login user, grab token, set user and current user&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    auth_data = login_user&lt;br /&gt;
    @token = auth_data[:token]&lt;br /&gt;
    @user = auth_data[:user]&lt;br /&gt;
    @current_user = @user&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # create default assignment and suggestion&lt;br /&gt;
  let(:assignment) { create(:assignment) }&lt;br /&gt;
  let(:suggestion) { create(:suggestion, assignment_id: assignment.id, user_id: @user.id) }&lt;br /&gt;
&lt;br /&gt;
  # Testing for add_comment method&lt;br /&gt;
  path '/api/v1/suggestions/{id}/add_comment' do&lt;br /&gt;
    post 'Add a comment to a suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
      parameter name: :comment, in: :body, schema: {&lt;br /&gt;
        type: :object,&lt;br /&gt;
        properties: {&lt;br /&gt;
          comment: { type: :string }&lt;br /&gt;
        },&lt;br /&gt;
        required: ['comment']&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      # successful comment addition test&lt;br /&gt;
      response '201', 'comment_added' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { { comment: 'This is a test comment' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['comment']).to eq('This is a test comment')&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for missing or empty comment&lt;br /&gt;
      response '422', 'unprocessable entity for missing or empty comment' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
        let(:comment) { '' }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          # Mock the params to simulate the request&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:require).with(:comment).and_return(comment)&lt;br /&gt;
          # Mock params[:id] and params[:comment] in the controller context&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:id).and_return(id)&lt;br /&gt;
          allow_any_instance_of(ActionController::Parameters).to receive(:[]).with(:comment).and_return(comment)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422) # Expect 400 if the comment is missing or empty&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
        let(:comment) { { comment: 'Invalid ID' } }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Tests for approving suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/approve' do&lt;br /&gt;
    post 'Approve suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # successful suggestion approval&lt;br /&gt;
        response '200', 'suggestion approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Approved')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable with a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for unprocessable without a record&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the approval process (e.g., ActiveRecord::RecordInvalid)&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:update_attribute).and_raise(ActiveRecord::RecordInvalid)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when a suggestion is not found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta_privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user as not having ta_privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for students not being able to approve suggestions&lt;br /&gt;
        response '403', 'students cannot approve suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for creating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions' do&lt;br /&gt;
    post 'Create suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :suggestion, in: :body, type: :object, required: true,&lt;br /&gt;
                description: 'Suggestion object with attributes'&lt;br /&gt;
&lt;br /&gt;
      # test for successful suggestion creation&lt;br /&gt;
      response '201', 'suggestion created successfully' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Suggestion title',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return properly for test&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(201)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['title']).to eq('Suggestion title')&lt;br /&gt;
          expect(data['description']).to eq('Detailed suggestion description')&lt;br /&gt;
          expect(data['auto_signup']).to eq(true)&lt;br /&gt;
          expect(data['status']).to eq('Initialized')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for a missing parameter&lt;br /&gt;
      response '422', 'missing title' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: '',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # set up current user to return current user properly&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
          data = JSON.parse(response.body)&lt;br /&gt;
          expect(data['error']).to eq('title is missing')&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for invalid suggestion given&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:suggestion) do&lt;br /&gt;
          {&lt;br /&gt;
            assignment_id: assignment.id,&lt;br /&gt;
            auto_signup: true,&lt;br /&gt;
            comment: 'This is a great suggestion!',&lt;br /&gt;
            description: 'Detailed suggestion description',&lt;br /&gt;
            title: 'Sample Suggestion',&lt;br /&gt;
            anonymous: false&lt;br /&gt;
          }&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
          suggestion = build(:suggestion, assignment_id: assignment.id, user_id: @user.id)&lt;br /&gt;
          allow(Suggestion).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for deleting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    delete 'Delete suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion deletion&lt;br /&gt;
        response '204', 'suggestion deleted' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(204)&lt;br /&gt;
            expect(response.body).to be_empty&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for record not being destroyed&lt;br /&gt;
        response '422', 'unprocessable entity' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulating an error in the deletion process&lt;br /&gt;
            allow_any_instance_of(Suggestion).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when suggestion is not found/doesn't exist&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test to make sure students cannot delete suggestions&lt;br /&gt;
        response '403', 'students cannot delete suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot delete suggestions.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for indexing/listing all suggestions&lt;br /&gt;
  path '/api/v1/suggestions/' do&lt;br /&gt;
    get 'list all suggestions' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
&lt;br /&gt;
      # test cases for when the user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful indexing&lt;br /&gt;
        response '200', 'suggestions listed' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test case for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for student to be forbidden from indexing suggestions&lt;br /&gt;
        response '403', 'students cannot index suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot view all suggestions of an assignment.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for rejecting suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}/reject' do&lt;br /&gt;
    post 'Reject suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is instructor/ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion rejection&lt;br /&gt;
        response '200', 'suggestion rejected' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['status']).to eq('Rejected')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for suggestion already being approved&lt;br /&gt;
        response '422', 'suggestion already approved' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          before do&lt;br /&gt;
            # Simulate suggestion status as 'Approved'&lt;br /&gt;
            suggestion.update!(status: 'Approved')&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(422)&lt;br /&gt;
            parsed_response = JSON.parse(response.body)&lt;br /&gt;
            expect(parsed_response['error']).to eq('Suggestion has already been approved.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for when the suggestion not being found&lt;br /&gt;
        response '404', 'suggestion not found' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(404)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privileges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
        end&lt;br /&gt;
        response '403', 'students cannot reject suggestions' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(403)&lt;br /&gt;
            expect(JSON.parse(response.body)['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for showing suggestions&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    get 'show suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # tests for when user is a instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
        response '200', 'suggestion details and comments returned' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            data = JSON.parse(response.body)&lt;br /&gt;
            expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
            expect(data['comments']).to be_an(Array)&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student/doesn't have ta privilges&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to not have ta privileges and make sure current user is set properly&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when user is owner of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          # make sure user is set as owner of suggestion&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
          # test for student being able to view their own suggestion&lt;br /&gt;
          response '200', 'student can view their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              data = JSON.parse(response.body)&lt;br /&gt;
              expect(data['suggestion']['id']).to eq(suggestion.id)&lt;br /&gt;
              expect(data['comments']).to be_an(Array)&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test case for when user is not owner of suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # make sure student can't view suggestions they don't own/are a part of&lt;br /&gt;
          response '403', 'student can\'t view other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for when suggestion doesn't exist&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # test cases for updating a suggestion&lt;br /&gt;
  path '/api/v1/suggestions/{id}' do&lt;br /&gt;
    patch 'update suggestion' do&lt;br /&gt;
      tags 'Suggestions'&lt;br /&gt;
      consumes 'application/json'&lt;br /&gt;
      parameter name: 'Authorization', in: :header, type: :string, required: true&lt;br /&gt;
      parameter name: :id, in: :path, type: :integer, required: true, description: 'ID of the suggestion'&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is an instructor/has ta privileges&lt;br /&gt;
      context '| when user is instructor | ' do&lt;br /&gt;
        # set user to have ta privileges&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(true)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test for successful suggestion update&lt;br /&gt;
        response '200', 'suggestion updated' do&lt;br /&gt;
          let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
          let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
          run_test! do |response|&lt;br /&gt;
            expect(response.status).to eq(200)&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test cases for when user is a student&lt;br /&gt;
      context ' | when user is student | ' do&lt;br /&gt;
        # set user to be a student and setup current user&lt;br /&gt;
        before(:each) do&lt;br /&gt;
          allow(AuthorizationHelper).to receive(:current_user_has_ta_privileges?).and_return(false)&lt;br /&gt;
          allow(controller).to receive(:current_user).and_return(@current_user)&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is owner/a part of suggestion&lt;br /&gt;
        context ' | when user is student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student owning the suggestion&lt;br /&gt;
            suggestion.update!(user_id: @user.id)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test to make sure students can update their own suggestion(s)&lt;br /&gt;
          response '200', 'student can update their own suggestion' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(200)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        # test cases for when student is not owner to suggestion&lt;br /&gt;
        context ' | when user is not student owner to suggestion | ' do&lt;br /&gt;
          before(:each) do&lt;br /&gt;
            # Simulate the student not owning the suggestion&lt;br /&gt;
            suggestion_double = double('Suggestion', id: suggestion.id, user_id: 9999)&lt;br /&gt;
            allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion_double)&lt;br /&gt;
          end&lt;br /&gt;
&lt;br /&gt;
          # test for forbidding student(s) from updating suggestions other then their own&lt;br /&gt;
          response '403', 'student can\'t updates other suggestions' do&lt;br /&gt;
            let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
            let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
            run_test! do |response|&lt;br /&gt;
              expect(response.status).to eq(403)&lt;br /&gt;
            end&lt;br /&gt;
          end&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion not being found&lt;br /&gt;
      response '404', 'suggestion not found' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { -1 }&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(404)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
&lt;br /&gt;
      # test for suggestion being invalid in some form/missing&lt;br /&gt;
      response '422', 'unprocessable entity' do&lt;br /&gt;
        let(:Authorization) { &amp;quot;Bearer #{@token}&amp;quot; }&lt;br /&gt;
        let(:id) { suggestion.id }&lt;br /&gt;
&lt;br /&gt;
        before do&lt;br /&gt;
          allow(Suggestion).to receive(:find).and_return(suggestion) # Mock finding the suggestion&lt;br /&gt;
          allow(suggestion).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(suggestion))&lt;br /&gt;
        end&lt;br /&gt;
&lt;br /&gt;
        run_test! do |response|&lt;br /&gt;
          expect(response.status).to eq(422)&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add detail to testing&lt;br /&gt;
* Add mailer email templates&lt;br /&gt;
* Record testing video&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Pranavi Sanganabhatla (psangan@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
* [# Testing video]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159488</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159488"/>
		<updated>2024-11-13T05:39:34Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
[[File:Class diagram suggestions controller.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
This is a pending change. Comments to the controller, helpers, and spec file have not been added or revised yet.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required:&lt;br /&gt;
## Update the suggestion's status to 'Approved'&lt;br /&gt;
## Create a topic entry in the database from the suggested topic&lt;br /&gt;
## Sign the suggester and his team up for the newly created topic&lt;br /&gt;
## Send out an email informing all teammates that the suggested topic was approved&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up_to_assignment_and_topic!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old method name !! Action !! New method name&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || no change || add_comment&lt;br /&gt;
|-&lt;br /&gt;
| list || renamed || index&lt;br /&gt;
|-&lt;br /&gt;
| student_view || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| student_edit || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| show || no change || show&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion || renamed || update&lt;br /&gt;
|-&lt;br /&gt;
| new || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| create || no change || create&lt;br /&gt;
|-&lt;br /&gt;
| submit || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| send_email || renamed || send_notice_of_approval!&lt;br /&gt;
|-&lt;br /&gt;
| notification || renamed|| sign_team_up_to_assignment_and_topic!&lt;br /&gt;
|-&lt;br /&gt;
| approve_suggestion || merged into approve || approve&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion || renamed || reject&lt;br /&gt;
|-&lt;br /&gt;
| suggestion_params || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| - || added || destroy&lt;br /&gt;
|-&lt;br /&gt;
| - || added || create_topic_from_suggestion!&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include PrivilegeHelper&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :ok&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      transaction do&lt;br /&gt;
        @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up_to_assignment_and_topic!&lt;br /&gt;
          send_notice_of_approval!&lt;br /&gt;
        end&lt;br /&gt;
        render json: @suggestion, status: :ok&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students cannot approve a suggestion.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      user_id: params[:suggestion_anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :ok&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def destroy&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      Suggestion.find(params[:id]).destroy!&lt;br /&gt;
      render json: {}, status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students do not have permission to delete suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def index&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      render json: Suggestion.where(assignment_id: params[:id]), status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students do not have permission to view all suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      suggestion = Suggestion.find(params[:id])&lt;br /&gt;
      if suggestion.status == 'Initialized'&lt;br /&gt;
        suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
        render json: suggestion, status: :ok&lt;br /&gt;
      else&lt;br /&gt;
        render json: { error: 'Suggestion has already been approved or rejected.' }, status: :unprocessable_entity&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students cannot reject a suggestion.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    puts @suggestion.user_id&lt;br /&gt;
    puts @current_user.id&lt;br /&gt;
    if @suggestion.user_id == @current_user.id || PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      render json: {&lt;br /&gt;
        suggestion: @suggestion,&lt;br /&gt;
        comments: SuggestionComment.where(suggestion_id: params[:id])&lt;br /&gt;
      }, status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students can only view their own suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title,&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval!&lt;br /&gt;
    Mailer.send_topic_approved_message(&lt;br /&gt;
      to: @suggester.email,&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      body: {&lt;br /&gt;
        approved_topic_name: @suggestion.title,&lt;br /&gt;
        suggester: @suggester.name&lt;br /&gt;
      }&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up_to_assignment_and_topic!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    if SignedUpTeam.exists?(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: 1).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML&lt;br /&gt;
* Methods have proper error handling&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-split into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up_to_assignment_and_topic!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries&lt;br /&gt;
* Each controller method uses a call to PrivilegeHelper instead of using the now deprecated 'allow_action?' method&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @body = defn[:body]&lt;br /&gt;
    @topic_name = defn[:body][:approved_topic_name]&lt;br /&gt;
    @proposer = defn[:body][:proposer]&lt;br /&gt;
&lt;br /&gt;
    defn[:to] = 'expertiza.mailer@gmail.com' if Rails.env.development? || Rails.env.test?&lt;br /&gt;
    mail(subject: defn[:subject], to: defn[:to], bcc: defn[:cc]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;privilege_helper.rb (Copied)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (privilege_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module PrivilegeHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The privilege helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the next section of this document.&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&lt;br /&gt;
9. Method: &amp;lt;code&amp;gt;create_topic_from_suggestion!&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates a suggestion using data from the existing suggestion and signs suggester up for topic&lt;br /&gt;
&lt;br /&gt;
10. Method: &amp;lt;code&amp;gt;send_notice_of_approval!&amp;lt;/code&amp;gt;&lt;br /&gt;
 * sends information to mailer to send an email to suggestion creator that their suggestion was approved&lt;br /&gt;
&lt;br /&gt;
=== Current Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Current&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def auth_token_header(user)&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
  { Authorization: &amp;quot;Bearer #{JSON.parse(response.body)['token']}&amp;quot; }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  let(:instructor) { instance_double(User, id: 6, role: 'instructor') }&lt;br /&gt;
  let(:student) { instance_double(User, id: 1, name: 'student_user', role: 'student', password: 'password') }&lt;br /&gt;
  let(:assignment) { instance_double(Assignment, id: 1, instructor:) }&lt;br /&gt;
  let(:suggestion) do&lt;br /&gt;
    instance_double(Suggestion, id: 1, assignment_id: assignment.id, user_id: student.id, title: 'Test Title')&lt;br /&gt;
  end&lt;br /&gt;
  let(:suggestion_comment) { instance_double(SuggestionComment) }&lt;br /&gt;
&lt;br /&gt;
  def stub_current_user(user)&lt;br /&gt;
    allow_any_instance_of(ApplicationController).to receive(:session).and_return({ user_id: user.id })&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    allow(Assignment).to receive(:find).with(assignment.id.to_s).and_return(assignment)&lt;br /&gt;
    allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion)&lt;br /&gt;
    stub_current_user(student)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  describe '#show' do&lt;br /&gt;
    context 'when user is authorized' do&lt;br /&gt;
      it 'returns suggestion details and comments' do&lt;br /&gt;
        student_institution = Institution.create!(name: 'North Carolina State University')&lt;br /&gt;
        student_role = Role.create!(name: 'Student')&lt;br /&gt;
        student_user = User.create!(name: 'student_user', password: 'password', email: 'example@gmail.com',&lt;br /&gt;
                                    full_name: 'Student User', role: student_role, institution: student_institution)&lt;br /&gt;
        student_suggestion = Suggestion.create!(title: 'Sample suggestion', description: 'Sample Text',&lt;br /&gt;
                                                status: 'Initialized', auto_signup: false, user_id: student_user)&lt;br /&gt;
&lt;br /&gt;
        get &amp;quot;/api/v1/suggestions/#{student_suggestion.id}&amp;quot;, headers: auth_token_header(student_user)&lt;br /&gt;
        expect(response).to have_http_status(:ok)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        puts parsed_response&lt;br /&gt;
        expect(parsed_response['suggestion']['id']).to eq(student_suggestion.id)&lt;br /&gt;
        expect(parsed_response['comments']).to be_an(Array)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    context 'when user is unauthorized' do&lt;br /&gt;
      it 'returns a forbidden error' do&lt;br /&gt;
        stub_current_user(instance_double(User, id: 2, role: 'student'))&lt;br /&gt;
&lt;br /&gt;
        get &amp;quot;/api/v1/suggestions/#{suggestion.id}&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:forbidden)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['error']).to eq('Students can only view their own suggestions.')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  describe '#add_comment' do&lt;br /&gt;
    it 'adds a comment and returns the comment JSON' do&lt;br /&gt;
      allow(SuggestionComment).to receive(:create!).and_return(suggestion_comment)&lt;br /&gt;
      allow(suggestion_comment).to receive(:as_json).and_return({ comment: 'Test comment' })&lt;br /&gt;
&lt;br /&gt;
      stub_current_user(student, 'student', student.role)&lt;br /&gt;
&lt;br /&gt;
      post &amp;quot;/api/v1/suggestions/#{suggestion.id}/comments&amp;quot;, params: { comment: 'Test comment' }&lt;br /&gt;
      expect(response).to have_http_status(:ok)&lt;br /&gt;
&lt;br /&gt;
      parsed_response = JSON.parse(response.body)&lt;br /&gt;
      expect(parsed_response['comment']).to eq('Test comment')&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    it 'returns an error when comment creation fails' do&lt;br /&gt;
      allow(SuggestionComment).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion_comment))&lt;br /&gt;
&lt;br /&gt;
      stub_current_user(student, 'student', student.role)&lt;br /&gt;
&lt;br /&gt;
      post &amp;quot;/api/v1/suggestions/#{suggestion.id}/comments&amp;quot;, params: { comment: '' }&lt;br /&gt;
      expect(response).to have_http_status(:unprocessable_entity)&lt;br /&gt;
&lt;br /&gt;
      parsed_response = JSON.parse(response.body)&lt;br /&gt;
      expect(parsed_response).to include('errors')&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  describe '#approve' do&lt;br /&gt;
    context 'when user is authorized' do&lt;br /&gt;
      it 'approves the suggestion and returns the updated suggestion JSON' do&lt;br /&gt;
        allow(suggestion).to receive(:update_attribute).with('status', 'Approved').and_return(true)&lt;br /&gt;
&lt;br /&gt;
        stub_current_user(instructor, 'instructor', instructor.role)&lt;br /&gt;
&lt;br /&gt;
        post &amp;quot;/api/v1/suggestions/#{suggestion.id}/approve&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:ok)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['status']).to eq('Approved')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    context 'when user is unauthorized' do&lt;br /&gt;
      it 'returns a forbidden error' do&lt;br /&gt;
        stub_current_user(student, 'student', student.role)&lt;br /&gt;
&lt;br /&gt;
        post &amp;quot;/api/v1/suggestions/#{suggestion.id}/approve&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:forbidden)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  describe '#reject' do&lt;br /&gt;
    context 'when user is authorized' do&lt;br /&gt;
      it 'rejects the suggestion and returns the updated suggestion JSON' do&lt;br /&gt;
        allow(suggestion).to receive(:update_attribute).with('status', 'Rejected').and_return(true)&lt;br /&gt;
&lt;br /&gt;
        stub_current_user(instructor, 'instructor', instructor.role)&lt;br /&gt;
&lt;br /&gt;
        post &amp;quot;/api/v1/suggestions/#{suggestion.id}/reject&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:ok)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['status']).to eq('Rejected')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    context 'when user is unauthorized' do&lt;br /&gt;
      it 'returns a forbidden error' do&lt;br /&gt;
        stub_current_user(student, 'student', student.role)&lt;br /&gt;
&lt;br /&gt;
        post &amp;quot;/api/v1/suggestions/#{suggestion.id}/reject&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:forbidden)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add comments into the code to improve code documentation&lt;br /&gt;
* Update test coverage to cover larger amounts of code&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Piyush Prasad (pprasad3@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=File:Class_diagram_suggestions_controller.png&amp;diff=159487</id>
		<title>File:Class diagram suggestions controller.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=File:Class_diagram_suggestions_controller.png&amp;diff=159487"/>
		<updated>2024-11-13T05:38:47Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: Spmclell uploaded a new version of File:Class diagram suggestions controller.png&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
class diagram for suggestions controller&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=File:Class_diagram_suggestions_controller.png&amp;diff=159486</id>
		<title>File:Class diagram suggestions controller.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=File:Class_diagram_suggestions_controller.png&amp;diff=159486"/>
		<updated>2024-11-13T05:28:32Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: class diagram for suggestions controller&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
class diagram for suggestions controller&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159436</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159436"/>
		<updated>2024-11-13T03:58:02Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Detailed Test Plan */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
This is a pending change. Comments to the controller, helpers, and spec file have not been added or revised yet.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required:&lt;br /&gt;
## Update the suggestion's status to 'Approved'&lt;br /&gt;
## Create a topic entry in the database from the suggested topic&lt;br /&gt;
## Sign the suggester and his team up for the newly created topic&lt;br /&gt;
## Send out an email informing all teammates that the suggested topic was approved&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up_to_assignment_and_topic!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old method name !! Action !! New method name&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || no change || add_comment&lt;br /&gt;
|-&lt;br /&gt;
| list || renamed || index&lt;br /&gt;
|-&lt;br /&gt;
| student_view || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| student_edit || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| show || no change || show&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion || renamed || update&lt;br /&gt;
|-&lt;br /&gt;
| new || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| create || no change || create&lt;br /&gt;
|-&lt;br /&gt;
| submit || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| send_email || renamed || send_notice_of_approval!&lt;br /&gt;
|-&lt;br /&gt;
| notification || renamed|| sign_team_up_to_assignment_and_topic!&lt;br /&gt;
|-&lt;br /&gt;
| approve_suggestion || merged into approve || approve&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion || renamed || reject&lt;br /&gt;
|-&lt;br /&gt;
| suggestion_params || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| - || added || destroy&lt;br /&gt;
|-&lt;br /&gt;
| - || added || create_topic_from_suggestion!&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include PrivilegeHelper&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :ok&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      transaction do&lt;br /&gt;
        @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up_to_assignment_and_topic!&lt;br /&gt;
          send_notice_of_approval!&lt;br /&gt;
        end&lt;br /&gt;
        render json: @suggestion, status: :ok&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students cannot approve a suggestion.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      user_id: params[:suggestion_anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :ok&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def destroy&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      Suggestion.find(params[:id]).destroy!&lt;br /&gt;
      render json: {}, status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students do not have permission to delete suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def index&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      render json: Suggestion.where(assignment_id: params[:id]), status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students do not have permission to view all suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      suggestion = Suggestion.find(params[:id])&lt;br /&gt;
      if suggestion.status == 'Initialized'&lt;br /&gt;
        suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
        render json: suggestion, status: :ok&lt;br /&gt;
      else&lt;br /&gt;
        render json: { error: 'Suggestion has already been approved or rejected.' }, status: :unprocessable_entity&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students cannot reject a suggestion.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    puts @suggestion.user_id&lt;br /&gt;
    puts @current_user.id&lt;br /&gt;
    if @suggestion.user_id == @current_user.id || PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      render json: {&lt;br /&gt;
        suggestion: @suggestion,&lt;br /&gt;
        comments: SuggestionComment.where(suggestion_id: params[:id])&lt;br /&gt;
      }, status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students can only view their own suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title,&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval!&lt;br /&gt;
    Mailer.send_topic_approved_message(&lt;br /&gt;
      to: @suggester.email,&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      body: {&lt;br /&gt;
        approved_topic_name: @suggestion.title,&lt;br /&gt;
        suggester: @suggester.name&lt;br /&gt;
      }&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up_to_assignment_and_topic!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    if SignedUpTeam.exists?(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: 1).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML&lt;br /&gt;
* Methods have proper error handling&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-split into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up_to_assignment_and_topic!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries&lt;br /&gt;
* Each controller method uses a call to PrivilegeHelper instead of using the now deprecated 'allow_action?' method&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @body = defn[:body]&lt;br /&gt;
    @topic_name = defn[:body][:approved_topic_name]&lt;br /&gt;
    @proposer = defn[:body][:proposer]&lt;br /&gt;
&lt;br /&gt;
    defn[:to] = 'expertiza.mailer@gmail.com' if Rails.env.development? || Rails.env.test?&lt;br /&gt;
    mail(subject: defn[:subject], to: defn[:to], bcc: defn[:cc]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;privilege_helper.rb (Copied)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (privilege_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module PrivilegeHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The privilege helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the next section of this document.&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt;&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&lt;br /&gt;
2. Method: &amp;lt;code&amp;gt;add_comment&amp;lt;/code&amp;gt;&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&lt;br /&gt;
3. Method: &amp;lt;code&amp;gt;approve&amp;lt;/code&amp;gt;&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&lt;br /&gt;
4. Method: &amp;lt;code&amp;gt;reject&amp;lt;/code&amp;gt;&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&lt;br /&gt;
5. Method: &amp;lt;code&amp;gt;edit&amp;lt;/code&amp;gt;&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&lt;br /&gt;
6. Method: &amp;lt;code&amp;gt;create&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&lt;br /&gt;
7. Method: &amp;lt;code&amp;gt;destroy&amp;lt;/code&amp;gt;&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&lt;br /&gt;
8. Method: &amp;lt;code&amp;gt;index&amp;lt;/code&amp;gt;&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&lt;br /&gt;
9. Method: &amp;lt;code&amp;gt;create_topic_from_suggestion!&amp;lt;/code&amp;gt;&lt;br /&gt;
 * creates a suggestion using data from the existing suggestion and signs suggester up for topic&lt;br /&gt;
&lt;br /&gt;
10. Method: &amp;lt;code&amp;gt;send_notice_of_approval!&amp;lt;/code&amp;gt;&lt;br /&gt;
 * sends information to mailer to send an email to suggestion creator that their suggestion was approved&lt;br /&gt;
&lt;br /&gt;
=== Current Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Current&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def auth_token_header(user)&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
  { Authorization: &amp;quot;Bearer #{JSON.parse(response.body)['token']}&amp;quot; }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  let(:instructor) { instance_double(User, id: 6, role: 'instructor') }&lt;br /&gt;
  let(:student) { instance_double(User, id: 1, name: 'student_user', role: 'student', password: 'password') }&lt;br /&gt;
  let(:assignment) { instance_double(Assignment, id: 1, instructor:) }&lt;br /&gt;
  let(:suggestion) do&lt;br /&gt;
    instance_double(Suggestion, id: 1, assignment_id: assignment.id, user_id: student.id, title: 'Test Title')&lt;br /&gt;
  end&lt;br /&gt;
  let(:suggestion_comment) { instance_double(SuggestionComment) }&lt;br /&gt;
&lt;br /&gt;
  def stub_current_user(user)&lt;br /&gt;
    allow_any_instance_of(ApplicationController).to receive(:session).and_return({ user_id: user.id })&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    allow(Assignment).to receive(:find).with(assignment.id.to_s).and_return(assignment)&lt;br /&gt;
    allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion)&lt;br /&gt;
    stub_current_user(student)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  describe '#show' do&lt;br /&gt;
    context 'when user is authorized' do&lt;br /&gt;
      it 'returns suggestion details and comments' do&lt;br /&gt;
        student_institution = Institution.create!(name: 'North Carolina State University')&lt;br /&gt;
        student_role = Role.create!(name: 'Student')&lt;br /&gt;
        student_user = User.create!(name: 'student_user', password: 'password', email: 'example@gmail.com',&lt;br /&gt;
                                    full_name: 'Student User', role: student_role, institution: student_institution)&lt;br /&gt;
        student_suggestion = Suggestion.create!(title: 'Sample suggestion', description: 'Sample Text',&lt;br /&gt;
                                                status: 'Initialized', auto_signup: false, user_id: student_user)&lt;br /&gt;
&lt;br /&gt;
        get &amp;quot;/api/v1/suggestions/#{student_suggestion.id}&amp;quot;, headers: auth_token_header(student_user)&lt;br /&gt;
        expect(response).to have_http_status(:ok)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        puts parsed_response&lt;br /&gt;
        expect(parsed_response['suggestion']['id']).to eq(student_suggestion.id)&lt;br /&gt;
        expect(parsed_response['comments']).to be_an(Array)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    context 'when user is unauthorized' do&lt;br /&gt;
      it 'returns a forbidden error' do&lt;br /&gt;
        stub_current_user(instance_double(User, id: 2, role: 'student'))&lt;br /&gt;
&lt;br /&gt;
        get &amp;quot;/api/v1/suggestions/#{suggestion.id}&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:forbidden)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['error']).to eq('Students can only view their own suggestions.')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  describe '#add_comment' do&lt;br /&gt;
    it 'adds a comment and returns the comment JSON' do&lt;br /&gt;
      allow(SuggestionComment).to receive(:create!).and_return(suggestion_comment)&lt;br /&gt;
      allow(suggestion_comment).to receive(:as_json).and_return({ comment: 'Test comment' })&lt;br /&gt;
&lt;br /&gt;
      stub_current_user(student, 'student', student.role)&lt;br /&gt;
&lt;br /&gt;
      post &amp;quot;/api/v1/suggestions/#{suggestion.id}/comments&amp;quot;, params: { comment: 'Test comment' }&lt;br /&gt;
      expect(response).to have_http_status(:ok)&lt;br /&gt;
&lt;br /&gt;
      parsed_response = JSON.parse(response.body)&lt;br /&gt;
      expect(parsed_response['comment']).to eq('Test comment')&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    it 'returns an error when comment creation fails' do&lt;br /&gt;
      allow(SuggestionComment).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion_comment))&lt;br /&gt;
&lt;br /&gt;
      stub_current_user(student, 'student', student.role)&lt;br /&gt;
&lt;br /&gt;
      post &amp;quot;/api/v1/suggestions/#{suggestion.id}/comments&amp;quot;, params: { comment: '' }&lt;br /&gt;
      expect(response).to have_http_status(:unprocessable_entity)&lt;br /&gt;
&lt;br /&gt;
      parsed_response = JSON.parse(response.body)&lt;br /&gt;
      expect(parsed_response).to include('errors')&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  describe '#approve' do&lt;br /&gt;
    context 'when user is authorized' do&lt;br /&gt;
      it 'approves the suggestion and returns the updated suggestion JSON' do&lt;br /&gt;
        allow(suggestion).to receive(:update_attribute).with('status', 'Approved').and_return(true)&lt;br /&gt;
&lt;br /&gt;
        stub_current_user(instructor, 'instructor', instructor.role)&lt;br /&gt;
&lt;br /&gt;
        post &amp;quot;/api/v1/suggestions/#{suggestion.id}/approve&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:ok)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['status']).to eq('Approved')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    context 'when user is unauthorized' do&lt;br /&gt;
      it 'returns a forbidden error' do&lt;br /&gt;
        stub_current_user(student, 'student', student.role)&lt;br /&gt;
&lt;br /&gt;
        post &amp;quot;/api/v1/suggestions/#{suggestion.id}/approve&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:forbidden)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  describe '#reject' do&lt;br /&gt;
    context 'when user is authorized' do&lt;br /&gt;
      it 'rejects the suggestion and returns the updated suggestion JSON' do&lt;br /&gt;
        allow(suggestion).to receive(:update_attribute).with('status', 'Rejected').and_return(true)&lt;br /&gt;
&lt;br /&gt;
        stub_current_user(instructor, 'instructor', instructor.role)&lt;br /&gt;
&lt;br /&gt;
        post &amp;quot;/api/v1/suggestions/#{suggestion.id}/reject&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:ok)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['status']).to eq('Rejected')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    context 'when user is unauthorized' do&lt;br /&gt;
      it 'returns a forbidden error' do&lt;br /&gt;
        stub_current_user(student, 'student', student.role)&lt;br /&gt;
&lt;br /&gt;
        post &amp;quot;/api/v1/suggestions/#{suggestion.id}/reject&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:forbidden)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add comments into the code to improve code documentation&lt;br /&gt;
* Update test coverage to cover larger amounts of code&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Piyush Prasad (pprasad3@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159434</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159434"/>
		<updated>2024-11-13T03:52:06Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Test Plan */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
This is a pending change. Comments to the controller, helpers, and spec file have not been added or revised yet.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required:&lt;br /&gt;
## Update the suggestion's status to 'Approved'&lt;br /&gt;
## Create a topic entry in the database from the suggested topic&lt;br /&gt;
## Sign the suggester and his team up for the newly created topic&lt;br /&gt;
## Send out an email informing all teammates that the suggested topic was approved&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up_to_assignment_and_topic!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old method name !! Action !! New method name&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || no change || add_comment&lt;br /&gt;
|-&lt;br /&gt;
| list || renamed || index&lt;br /&gt;
|-&lt;br /&gt;
| student_view || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| student_edit || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| show || no change || show&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion || renamed || update&lt;br /&gt;
|-&lt;br /&gt;
| new || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| create || no change || create&lt;br /&gt;
|-&lt;br /&gt;
| submit || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| send_email || renamed || send_notice_of_approval!&lt;br /&gt;
|-&lt;br /&gt;
| notification || renamed|| sign_team_up_to_assignment_and_topic!&lt;br /&gt;
|-&lt;br /&gt;
| approve_suggestion || merged into approve || approve&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion || renamed || reject&lt;br /&gt;
|-&lt;br /&gt;
| suggestion_params || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| - || added || destroy&lt;br /&gt;
|-&lt;br /&gt;
| - || added || create_topic_from_suggestion!&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include PrivilegeHelper&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :ok&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      transaction do&lt;br /&gt;
        @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up_to_assignment_and_topic!&lt;br /&gt;
          send_notice_of_approval!&lt;br /&gt;
        end&lt;br /&gt;
        render json: @suggestion, status: :ok&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students cannot approve a suggestion.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      user_id: params[:suggestion_anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :ok&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def destroy&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      Suggestion.find(params[:id]).destroy!&lt;br /&gt;
      render json: {}, status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students do not have permission to delete suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def index&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      render json: Suggestion.where(assignment_id: params[:id]), status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students do not have permission to view all suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      suggestion = Suggestion.find(params[:id])&lt;br /&gt;
      if suggestion.status == 'Initialized'&lt;br /&gt;
        suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
        render json: suggestion, status: :ok&lt;br /&gt;
      else&lt;br /&gt;
        render json: { error: 'Suggestion has already been approved or rejected.' }, status: :unprocessable_entity&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students cannot reject a suggestion.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    puts @suggestion.user_id&lt;br /&gt;
    puts @current_user.id&lt;br /&gt;
    if @suggestion.user_id == @current_user.id || PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      render json: {&lt;br /&gt;
        suggestion: @suggestion,&lt;br /&gt;
        comments: SuggestionComment.where(suggestion_id: params[:id])&lt;br /&gt;
      }, status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students can only view their own suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title,&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval!&lt;br /&gt;
    Mailer.send_topic_approved_message(&lt;br /&gt;
      to: @suggester.email,&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      body: {&lt;br /&gt;
        approved_topic_name: @suggestion.title,&lt;br /&gt;
        suggester: @suggester.name&lt;br /&gt;
      }&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up_to_assignment_and_topic!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    if SignedUpTeam.exists?(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: 1).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML&lt;br /&gt;
* Methods have proper error handling&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-split into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up_to_assignment_and_topic!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries&lt;br /&gt;
* Each controller method uses a call to PrivilegeHelper instead of using the now deprecated 'allow_action?' method&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @body = defn[:body]&lt;br /&gt;
    @topic_name = defn[:body][:approved_topic_name]&lt;br /&gt;
    @proposer = defn[:body][:proposer]&lt;br /&gt;
&lt;br /&gt;
    defn[:to] = 'expertiza.mailer@gmail.com' if Rails.env.development? || Rails.env.test?&lt;br /&gt;
    mail(subject: defn[:subject], to: defn[:to], bcc: defn[:cc]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;privilege_helper.rb (Copied)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (privilege_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module PrivilegeHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The privilege helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the next section of this document.&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: show&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&lt;br /&gt;
2. Method: add_comment&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&lt;br /&gt;
3. Method: approve&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&lt;br /&gt;
4. Method: reject&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&lt;br /&gt;
5. Method: edit&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&lt;br /&gt;
6. Method: create&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&lt;br /&gt;
7. Method: destroy&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&lt;br /&gt;
8. Method: index&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&lt;br /&gt;
9. Method: create_topic_from_suggestion&lt;br /&gt;
 * creates a suggestion using data from the existing suggestion and signs suggester up for topic&lt;br /&gt;
&lt;br /&gt;
10. Method: send_notice_of_approval&lt;br /&gt;
 * sends information to mailer to send an email to suggestion creator that their suggestion was approved&lt;br /&gt;
&lt;br /&gt;
=== Current Spec file ===&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller_spec.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Current&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
require 'swagger_helper'&lt;br /&gt;
require 'rails_helper'&lt;br /&gt;
&lt;br /&gt;
def auth_token_header(user)&lt;br /&gt;
  post '/login', params: { user_name: user.name, password: 'password' }&lt;br /&gt;
  { Authorization: &amp;quot;Bearer #{JSON.parse(response.body)['token']}&amp;quot; }&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
RSpec.describe 'Suggestions API', type: :request do&lt;br /&gt;
  let(:instructor) { instance_double(User, id: 6, role: 'instructor') }&lt;br /&gt;
  let(:student) { instance_double(User, id: 1, name: 'student_user', role: 'student', password: 'password') }&lt;br /&gt;
  let(:assignment) { instance_double(Assignment, id: 1, instructor:) }&lt;br /&gt;
  let(:suggestion) do&lt;br /&gt;
    instance_double(Suggestion, id: 1, assignment_id: assignment.id, user_id: student.id, title: 'Test Title')&lt;br /&gt;
  end&lt;br /&gt;
  let(:suggestion_comment) { instance_double(SuggestionComment) }&lt;br /&gt;
&lt;br /&gt;
  def stub_current_user(user)&lt;br /&gt;
    allow_any_instance_of(ApplicationController).to receive(:session).and_return({ user_id: user.id })&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  before(:each) do&lt;br /&gt;
    allow(Assignment).to receive(:find).with(assignment.id.to_s).and_return(assignment)&lt;br /&gt;
    allow(Suggestion).to receive(:find).with(suggestion.id.to_s).and_return(suggestion)&lt;br /&gt;
    stub_current_user(student)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  describe '#show' do&lt;br /&gt;
    context 'when user is authorized' do&lt;br /&gt;
      it 'returns suggestion details and comments' do&lt;br /&gt;
        student_institution = Institution.create!(name: 'North Carolina State University')&lt;br /&gt;
        student_role = Role.create!(name: 'Student')&lt;br /&gt;
        student_user = User.create!(name: 'student_user', password: 'password', email: 'example@gmail.com',&lt;br /&gt;
                                    full_name: 'Student User', role: student_role, institution: student_institution)&lt;br /&gt;
        student_suggestion = Suggestion.create!(title: 'Sample suggestion', description: 'Sample Text',&lt;br /&gt;
                                                status: 'Initialized', auto_signup: false, user_id: student_user)&lt;br /&gt;
&lt;br /&gt;
        get &amp;quot;/api/v1/suggestions/#{student_suggestion.id}&amp;quot;, headers: auth_token_header(student_user)&lt;br /&gt;
        expect(response).to have_http_status(:ok)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        puts parsed_response&lt;br /&gt;
        expect(parsed_response['suggestion']['id']).to eq(student_suggestion.id)&lt;br /&gt;
        expect(parsed_response['comments']).to be_an(Array)&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    context 'when user is unauthorized' do&lt;br /&gt;
      it 'returns a forbidden error' do&lt;br /&gt;
        stub_current_user(instance_double(User, id: 2, role: 'student'))&lt;br /&gt;
&lt;br /&gt;
        get &amp;quot;/api/v1/suggestions/#{suggestion.id}&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:forbidden)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['error']).to eq('Students can only view their own suggestions.')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  describe '#add_comment' do&lt;br /&gt;
    it 'adds a comment and returns the comment JSON' do&lt;br /&gt;
      allow(SuggestionComment).to receive(:create!).and_return(suggestion_comment)&lt;br /&gt;
      allow(suggestion_comment).to receive(:as_json).and_return({ comment: 'Test comment' })&lt;br /&gt;
&lt;br /&gt;
      stub_current_user(student, 'student', student.role)&lt;br /&gt;
&lt;br /&gt;
      post &amp;quot;/api/v1/suggestions/#{suggestion.id}/comments&amp;quot;, params: { comment: 'Test comment' }&lt;br /&gt;
      expect(response).to have_http_status(:ok)&lt;br /&gt;
&lt;br /&gt;
      parsed_response = JSON.parse(response.body)&lt;br /&gt;
      expect(parsed_response['comment']).to eq('Test comment')&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    it 'returns an error when comment creation fails' do&lt;br /&gt;
      allow(SuggestionComment).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(suggestion_comment))&lt;br /&gt;
&lt;br /&gt;
      stub_current_user(student, 'student', student.role)&lt;br /&gt;
&lt;br /&gt;
      post &amp;quot;/api/v1/suggestions/#{suggestion.id}/comments&amp;quot;, params: { comment: '' }&lt;br /&gt;
      expect(response).to have_http_status(:unprocessable_entity)&lt;br /&gt;
&lt;br /&gt;
      parsed_response = JSON.parse(response.body)&lt;br /&gt;
      expect(parsed_response).to include('errors')&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  describe '#approve' do&lt;br /&gt;
    context 'when user is authorized' do&lt;br /&gt;
      it 'approves the suggestion and returns the updated suggestion JSON' do&lt;br /&gt;
        allow(suggestion).to receive(:update_attribute).with('status', 'Approved').and_return(true)&lt;br /&gt;
&lt;br /&gt;
        stub_current_user(instructor, 'instructor', instructor.role)&lt;br /&gt;
&lt;br /&gt;
        post &amp;quot;/api/v1/suggestions/#{suggestion.id}/approve&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:ok)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['status']).to eq('Approved')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    context 'when user is unauthorized' do&lt;br /&gt;
      it 'returns a forbidden error' do&lt;br /&gt;
        stub_current_user(student, 'student', student.role)&lt;br /&gt;
&lt;br /&gt;
        post &amp;quot;/api/v1/suggestions/#{suggestion.id}/approve&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:forbidden)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['error']).to eq('Students cannot approve a suggestion.')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  describe '#reject' do&lt;br /&gt;
    context 'when user is authorized' do&lt;br /&gt;
      it 'rejects the suggestion and returns the updated suggestion JSON' do&lt;br /&gt;
        allow(suggestion).to receive(:update_attribute).with('status', 'Rejected').and_return(true)&lt;br /&gt;
&lt;br /&gt;
        stub_current_user(instructor, 'instructor', instructor.role)&lt;br /&gt;
&lt;br /&gt;
        post &amp;quot;/api/v1/suggestions/#{suggestion.id}/reject&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:ok)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['status']).to eq('Rejected')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    context 'when user is unauthorized' do&lt;br /&gt;
      it 'returns a forbidden error' do&lt;br /&gt;
        stub_current_user(student, 'student', student.role)&lt;br /&gt;
&lt;br /&gt;
        post &amp;quot;/api/v1/suggestions/#{suggestion.id}/reject&amp;quot;&lt;br /&gt;
        expect(response).to have_http_status(:forbidden)&lt;br /&gt;
&lt;br /&gt;
        parsed_response = JSON.parse(response.body)&lt;br /&gt;
        expect(parsed_response['error']).to eq('Students cannot reject a suggestion.')&lt;br /&gt;
      end&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add comments into the code to improve code documentation&lt;br /&gt;
* Update test coverage to cover larger amounts of code&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Piyush Prasad (pprasad3@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159429</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159429"/>
		<updated>2024-11-13T03:34:08Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Detailed Test Plan */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
This is a pending change. Comments to the controller, helpers, and spec file have not been added or revised yet.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required:&lt;br /&gt;
## Update the suggestion's status to 'Approved'&lt;br /&gt;
## Create a topic entry in the database from the suggested topic&lt;br /&gt;
## Sign the suggester and his team up for the newly created topic&lt;br /&gt;
## Send out an email informing all teammates that the suggested topic was approved&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up_to_assignment_and_topic!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old method name !! Action !! New method name&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || no change || add_comment&lt;br /&gt;
|-&lt;br /&gt;
| list || renamed || index&lt;br /&gt;
|-&lt;br /&gt;
| student_view || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| student_edit || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| show || no change || show&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion || renamed || update&lt;br /&gt;
|-&lt;br /&gt;
| new || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| create || no change || create&lt;br /&gt;
|-&lt;br /&gt;
| submit || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| send_email || renamed || send_notice_of_approval!&lt;br /&gt;
|-&lt;br /&gt;
| notification || renamed|| sign_team_up_to_assignment_and_topic!&lt;br /&gt;
|-&lt;br /&gt;
| approve_suggestion || merged into approve || approve&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion || renamed || reject&lt;br /&gt;
|-&lt;br /&gt;
| suggestion_params || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| - || added || destroy&lt;br /&gt;
|-&lt;br /&gt;
| - || added || create_topic_from_suggestion!&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include PrivilegeHelper&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :ok&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      transaction do&lt;br /&gt;
        @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up_to_assignment_and_topic!&lt;br /&gt;
          send_notice_of_approval!&lt;br /&gt;
        end&lt;br /&gt;
        render json: @suggestion, status: :ok&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students cannot approve a suggestion.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      user_id: params[:suggestion_anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :ok&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def destroy&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      Suggestion.find(params[:id]).destroy!&lt;br /&gt;
      render json: {}, status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students do not have permission to delete suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def index&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      render json: Suggestion.where(assignment_id: params[:id]), status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students do not have permission to view all suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      suggestion = Suggestion.find(params[:id])&lt;br /&gt;
      if suggestion.status == 'Initialized'&lt;br /&gt;
        suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
        render json: suggestion, status: :ok&lt;br /&gt;
      else&lt;br /&gt;
        render json: { error: 'Suggestion has already been approved or rejected.' }, status: :unprocessable_entity&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students cannot reject a suggestion.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    puts @suggestion.user_id&lt;br /&gt;
    puts @current_user.id&lt;br /&gt;
    if @suggestion.user_id == @current_user.id || PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      render json: {&lt;br /&gt;
        suggestion: @suggestion,&lt;br /&gt;
        comments: SuggestionComment.where(suggestion_id: params[:id])&lt;br /&gt;
      }, status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students can only view their own suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title,&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval!&lt;br /&gt;
    Mailer.send_topic_approved_message(&lt;br /&gt;
      to: @suggester.email,&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      body: {&lt;br /&gt;
        approved_topic_name: @suggestion.title,&lt;br /&gt;
        suggester: @suggester.name&lt;br /&gt;
      }&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up_to_assignment_and_topic!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    if SignedUpTeam.exists?(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: 1).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML&lt;br /&gt;
* Methods have proper error handling&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-split into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up_to_assignment_and_topic!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries&lt;br /&gt;
* Each controller method uses a call to PrivilegeHelper instead of using the now deprecated 'allow_action?' method&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @body = defn[:body]&lt;br /&gt;
    @topic_name = defn[:body][:approved_topic_name]&lt;br /&gt;
    @proposer = defn[:body][:proposer]&lt;br /&gt;
&lt;br /&gt;
    defn[:to] = 'expertiza.mailer@gmail.com' if Rails.env.development? || Rails.env.test?&lt;br /&gt;
    mail(subject: defn[:subject], to: defn[:to], bcc: defn[:cc]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;privilege_helper.rb (Copied)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (privilege_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module PrivilegeHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The privilege helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the next section of this document.&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: show&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&lt;br /&gt;
2. Method: add_comment&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&lt;br /&gt;
3. Method: approve&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&lt;br /&gt;
4. Method: reject&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&lt;br /&gt;
5. Method: edit&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&lt;br /&gt;
6. Method: create&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&lt;br /&gt;
7. Method: destroy&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&lt;br /&gt;
8. Method: index&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&lt;br /&gt;
9. Method: create_topic_from_suggestion&lt;br /&gt;
 * creates a suggestion using data from the existing suggestion and signs suggester up for topic&lt;br /&gt;
&lt;br /&gt;
10. Method: send_notice_of_approval&lt;br /&gt;
 * sends information to mailer to send an email to suggestion creator that their suggestion was approved&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add comments into the code to improve code documentation&lt;br /&gt;
* Update test coverage to cover larger amounts of code&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Piyush Prasad (pprasad3@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159425</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159425"/>
		<updated>2024-11-13T03:27:39Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Next Steps */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
This is a pending change. Comments to the controller, helpers, and spec file have not been added or revised yet.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required:&lt;br /&gt;
## Update the suggestion's status to 'Approved'&lt;br /&gt;
## Create a topic entry in the database from the suggested topic&lt;br /&gt;
## Sign the suggester and his team up for the newly created topic&lt;br /&gt;
## Send out an email informing all teammates that the suggested topic was approved&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up_to_assignment_and_topic!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old method name !! Action !! New method name&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || no change || add_comment&lt;br /&gt;
|-&lt;br /&gt;
| list || renamed || index&lt;br /&gt;
|-&lt;br /&gt;
| student_view || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| student_edit || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| show || no change || show&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion || renamed || update&lt;br /&gt;
|-&lt;br /&gt;
| new || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| create || no change || create&lt;br /&gt;
|-&lt;br /&gt;
| submit || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| send_email || renamed || send_notice_of_approval!&lt;br /&gt;
|-&lt;br /&gt;
| notification || renamed|| sign_team_up_to_assignment_and_topic!&lt;br /&gt;
|-&lt;br /&gt;
| approve_suggestion || merged into approve || approve&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion || renamed || reject&lt;br /&gt;
|-&lt;br /&gt;
| suggestion_params || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| - || added || destroy&lt;br /&gt;
|-&lt;br /&gt;
| - || added || create_topic_from_suggestion!&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include PrivilegeHelper&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :ok&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      transaction do&lt;br /&gt;
        @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up_to_assignment_and_topic!&lt;br /&gt;
          send_notice_of_approval!&lt;br /&gt;
        end&lt;br /&gt;
        render json: @suggestion, status: :ok&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students cannot approve a suggestion.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      user_id: params[:suggestion_anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :ok&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def destroy&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      Suggestion.find(params[:id]).destroy!&lt;br /&gt;
      render json: {}, status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students do not have permission to delete suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def index&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      render json: Suggestion.where(assignment_id: params[:id]), status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students do not have permission to view all suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      suggestion = Suggestion.find(params[:id])&lt;br /&gt;
      if suggestion.status == 'Initialized'&lt;br /&gt;
        suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
        render json: suggestion, status: :ok&lt;br /&gt;
      else&lt;br /&gt;
        render json: { error: 'Suggestion has already been approved or rejected.' }, status: :unprocessable_entity&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students cannot reject a suggestion.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    puts @suggestion.user_id&lt;br /&gt;
    puts @current_user.id&lt;br /&gt;
    if @suggestion.user_id == @current_user.id || PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      render json: {&lt;br /&gt;
        suggestion: @suggestion,&lt;br /&gt;
        comments: SuggestionComment.where(suggestion_id: params[:id])&lt;br /&gt;
      }, status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students can only view their own suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title,&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval!&lt;br /&gt;
    Mailer.send_topic_approved_message(&lt;br /&gt;
      to: @suggester.email,&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      body: {&lt;br /&gt;
        approved_topic_name: @suggestion.title,&lt;br /&gt;
        suggester: @suggester.name&lt;br /&gt;
      }&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up_to_assignment_and_topic!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    if SignedUpTeam.exists?(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: 1).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML&lt;br /&gt;
* Methods have proper error handling&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-split into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up_to_assignment_and_topic!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries&lt;br /&gt;
* Each controller method uses a call to PrivilegeHelper instead of using the now deprecated 'allow_action?' method&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @body = defn[:body]&lt;br /&gt;
    @topic_name = defn[:body][:approved_topic_name]&lt;br /&gt;
    @proposer = defn[:body][:proposer]&lt;br /&gt;
&lt;br /&gt;
    defn[:to] = 'expertiza.mailer@gmail.com' if Rails.env.development? || Rails.env.test?&lt;br /&gt;
    mail(subject: defn[:subject], to: defn[:to], bcc: defn[:cc]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;privilege_helper.rb (Copied)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (privilege_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module PrivilegeHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The privilege helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the next section of this document.&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: Show&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&lt;br /&gt;
2. Method: add_comment&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&lt;br /&gt;
3. Method: approve&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&lt;br /&gt;
4. Method: reject&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&lt;br /&gt;
5. Method: edit&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&lt;br /&gt;
6. Method: create&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&lt;br /&gt;
7. Method: destroy&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&lt;br /&gt;
8. Method: index&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&lt;br /&gt;
9. Method: create_topic_from_suggestion&lt;br /&gt;
 * creates a suggestion using data from the existing suggestion and signs suggester up for topic&lt;br /&gt;
&lt;br /&gt;
10. Method: send notice of approval&lt;br /&gt;
 * sends information to mailer to send an email to suggestion creator that their suggestion was approved&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add comments into the code to improve code documentation&lt;br /&gt;
* Update test coverage to cover larger amounts of code&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Piyush Prasad (pprasad3@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159420</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159420"/>
		<updated>2024-11-13T03:23:14Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Code coverage */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
This is a pending change. Comments to the controller, helpers, and spec file have not been added or revised yet.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required:&lt;br /&gt;
## Update the suggestion's status to 'Approved'&lt;br /&gt;
## Create a topic entry in the database from the suggested topic&lt;br /&gt;
## Sign the suggester and his team up for the newly created topic&lt;br /&gt;
## Send out an email informing all teammates that the suggested topic was approved&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up_to_assignment_and_topic!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old method name !! Action !! New method name&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || no change || add_comment&lt;br /&gt;
|-&lt;br /&gt;
| list || renamed || index&lt;br /&gt;
|-&lt;br /&gt;
| student_view || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| student_edit || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| show || no change || show&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion || renamed || update&lt;br /&gt;
|-&lt;br /&gt;
| new || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| create || no change || create&lt;br /&gt;
|-&lt;br /&gt;
| submit || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| send_email || renamed || send_notice_of_approval!&lt;br /&gt;
|-&lt;br /&gt;
| notification || renamed|| sign_team_up_to_assignment_and_topic!&lt;br /&gt;
|-&lt;br /&gt;
| approve_suggestion || merged into approve || approve&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion || renamed || reject&lt;br /&gt;
|-&lt;br /&gt;
| suggestion_params || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| - || added || destroy&lt;br /&gt;
|-&lt;br /&gt;
| - || added || create_topic_from_suggestion!&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include PrivilegeHelper&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :ok&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      transaction do&lt;br /&gt;
        @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up_to_assignment_and_topic!&lt;br /&gt;
          send_notice_of_approval!&lt;br /&gt;
        end&lt;br /&gt;
        render json: @suggestion, status: :ok&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students cannot approve a suggestion.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      user_id: params[:suggestion_anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :ok&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def destroy&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      Suggestion.find(params[:id]).destroy!&lt;br /&gt;
      render json: {}, status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students do not have permission to delete suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def index&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      render json: Suggestion.where(assignment_id: params[:id]), status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students do not have permission to view all suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      suggestion = Suggestion.find(params[:id])&lt;br /&gt;
      if suggestion.status == 'Initialized'&lt;br /&gt;
        suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
        render json: suggestion, status: :ok&lt;br /&gt;
      else&lt;br /&gt;
        render json: { error: 'Suggestion has already been approved or rejected.' }, status: :unprocessable_entity&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students cannot reject a suggestion.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    puts @suggestion.user_id&lt;br /&gt;
    puts @current_user.id&lt;br /&gt;
    if @suggestion.user_id == @current_user.id || PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      render json: {&lt;br /&gt;
        suggestion: @suggestion,&lt;br /&gt;
        comments: SuggestionComment.where(suggestion_id: params[:id])&lt;br /&gt;
      }, status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students can only view their own suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title,&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval!&lt;br /&gt;
    Mailer.send_topic_approved_message(&lt;br /&gt;
      to: @suggester.email,&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      body: {&lt;br /&gt;
        approved_topic_name: @suggestion.title,&lt;br /&gt;
        suggester: @suggester.name&lt;br /&gt;
      }&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up_to_assignment_and_topic!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    if SignedUpTeam.exists?(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: 1).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML&lt;br /&gt;
* Methods have proper error handling&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-split into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up_to_assignment_and_topic!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries&lt;br /&gt;
* Each controller method uses a call to PrivilegeHelper instead of using the now deprecated 'allow_action?' method&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @body = defn[:body]&lt;br /&gt;
    @topic_name = defn[:body][:approved_topic_name]&lt;br /&gt;
    @proposer = defn[:body][:proposer]&lt;br /&gt;
&lt;br /&gt;
    defn[:to] = 'expertiza.mailer@gmail.com' if Rails.env.development? || Rails.env.test?&lt;br /&gt;
    mail(subject: defn[:subject], to: defn[:to], bcc: defn[:cc]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;privilege_helper.rb (Copied)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (privilege_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module PrivilegeHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The privilege helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the next section of this document.&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Detailed Test Plan===&lt;br /&gt;
&lt;br /&gt;
1. Method: Show&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&lt;br /&gt;
2. Method: add_comment&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&lt;br /&gt;
3. Method: approve&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&lt;br /&gt;
4. Method: reject&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&lt;br /&gt;
5. Method: edit&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&lt;br /&gt;
6. Method: create&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&lt;br /&gt;
7. Method: destroy&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&lt;br /&gt;
8. Method: index&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&lt;br /&gt;
9. Method: create_topic_from_suggestion&lt;br /&gt;
 * creates a suggestion using data from the existing suggestion and signs suggester up for topic&lt;br /&gt;
&lt;br /&gt;
10. Method: send notice of approval&lt;br /&gt;
 * sends information to mailer to send an email to suggestion creator that their suggestion was approved&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add comments into the code to improve code documentation&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Piyush Prasad (pprasad3@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159419</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159419"/>
		<updated>2024-11-13T03:22:24Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Test Plan */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
* '''Enhance Documentation''': Add comprehensive comments throughout the controller, clearly explaining the purpose and functionality of custom methods&lt;br /&gt;
* '''Reimplement Notification Method''': Simplify the notification method by refining its control logic and reducing complexity for better readability and efficiency. Additionally, &amp;quot;notification&amp;quot; is a poor name; it should be renamed to a verb that clearly indicates the action being performed&lt;br /&gt;
* '''Clarify Method Names''':&lt;br /&gt;
** Rename reject_suggestion to reject for clarity&lt;br /&gt;
** Rename update_suggestion to update to better reflect its functionality&lt;br /&gt;
** Rename approve_suggestion to approve for clarity&lt;br /&gt;
* '''Refactor Email Sending Logic''': Move the send_email method to the Mailer class located in app/mailers/mailer.rb to separate concerns and streamline the code&lt;br /&gt;
* '''Address DRY Violations''': In views/suggestion/show.html.erb and views/suggestion/student_view.html.erb, identify and resolve the DRY (Don't Repeat Yourself) violation by merging these two files into a single view to enhance maintainability&lt;br /&gt;
* '''Update Tests''': Revise existing tests to ensure they pass after the changes. Additionally, improve test coverage for the controller by adding new tests to verify the updated functionality&lt;br /&gt;
* During the reimplementation, review the structure and functionality of existing suggestion-related classes for insights and best practices. Remember, this is a complete reimplementation, not a refactor&lt;br /&gt;
&lt;br /&gt;
Since this is a reimplementation project, some functionalities may be altered. Ensure that all changes are properly tested and that existing tests are updated to reflect these modifications.&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
===Enhance Documentation===&lt;br /&gt;
&lt;br /&gt;
This is a pending change. Comments to the controller, helpers, and spec file have not been added or revised yet.&lt;br /&gt;
&lt;br /&gt;
===Reimplement Notification Method===&lt;br /&gt;
&lt;br /&gt;
The original method is hard to follow due to its nested if-else blocks and performing of multiple tasks. Additionally, the related functionality is split among all methods that are part of the approval process, making the final outcome difficult to follow.&lt;br /&gt;
&lt;br /&gt;
The entire approval process has been reimplemented to create logical segmentation and easy to follow and understand methods.&lt;br /&gt;
# 'approve_suggestion' is now 'approve', and performs the four high-level steps required:&lt;br /&gt;
## Update the suggestion's status to 'Approved'&lt;br /&gt;
## Create a topic entry in the database from the suggested topic&lt;br /&gt;
## Sign the suggester and his team up for the newly created topic&lt;br /&gt;
## Send out an email informing all teammates that the suggested topic was approved&lt;br /&gt;
# 'create_topic_from_suggestion!' is simply a wrapper method which creates a new record of the SignUpTopic model. It exists to keep the overarching 'approve' method short&lt;br /&gt;
# 'sign_team_up_to_assignment_and_topic!' performs the necessary steps to signing the student and his team up for the newly created topic. It creates a new team if necessary and de-registers any waitlisted assignments that the team might have&lt;br /&gt;
# 'send_notice_of_approval!' sends out the notification email informing all team members that their topic has been approved&lt;br /&gt;
&lt;br /&gt;
===Clarify Method Names===&lt;br /&gt;
&lt;br /&gt;
Each method has been examined for relevance and conciseness. Most methods are renamed to standard rails controller methods, or now exhibit rails naming convention. These would be:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old method name !! Action !! New method name&lt;br /&gt;
|-&lt;br /&gt;
| add_comment || no change || add_comment&lt;br /&gt;
|-&lt;br /&gt;
| list || renamed || index&lt;br /&gt;
|-&lt;br /&gt;
| student_view || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| student_edit || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| show || no change || show&lt;br /&gt;
|-&lt;br /&gt;
| update_suggestion || renamed || update&lt;br /&gt;
|-&lt;br /&gt;
| new || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| create || no change || create&lt;br /&gt;
|-&lt;br /&gt;
| submit || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| send_email || renamed || send_notice_of_approval!&lt;br /&gt;
|-&lt;br /&gt;
| notification || renamed|| sign_team_up_to_assignment_and_topic!&lt;br /&gt;
|-&lt;br /&gt;
| approve_suggestion || merged into approve || approve&lt;br /&gt;
|-&lt;br /&gt;
| reject_suggestion || renamed || reject&lt;br /&gt;
|-&lt;br /&gt;
| suggestion_params || removed || -&lt;br /&gt;
|-&lt;br /&gt;
| - || added || destroy&lt;br /&gt;
|-&lt;br /&gt;
| - || added || create_topic_from_suggestion!&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
===Refactor Email Sending Logic===&lt;br /&gt;
&lt;br /&gt;
The original implementation first constructs a list of recipients iteratively. This adds to the length of the method, but also causes and increased number of database queries, slowing the program down.&lt;br /&gt;
&lt;br /&gt;
In the reimplemented version, the code that finds the recipients' emails has been compacted into a single query chain, and thus inlined into the call to the Mailer class. The act of sending the email is moved into the Mailer helper class to separate concerns and streamline the code.&lt;br /&gt;
&lt;br /&gt;
===Address DRY Violations===&lt;br /&gt;
&lt;br /&gt;
Since this project is to reimplement the controller as an API which works with JSON only, there are no view files and thus this objective does not require any action. It is perhaps a holdover from before the decision to split Expertiza into frontend and backend.&lt;br /&gt;
&lt;br /&gt;
===Update Tests===&lt;br /&gt;
&lt;br /&gt;
Currently, this task is being worked on and is not complete yet.&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
===Models:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Suggestion &amp;lt; ApplicationRecord&lt;br /&gt;
  has_many :suggestion_comments, dependent: :delete_all&lt;br /&gt;
&lt;br /&gt;
  validates :title, uniqueness: { case_sensitive: false }&lt;br /&gt;
  validates :description, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestion_comment.rb (Modified)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  validates :comments, presence: true&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionComment &amp;lt; ApplicationRecord&lt;br /&gt;
  belongs_to :suggestion&lt;br /&gt;
  belongs_to :user&lt;br /&gt;
&lt;br /&gt;
  validates :comment, presence: true&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029234902_create_suggestions.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestions &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestions do |t|&lt;br /&gt;
      t.string :title&lt;br /&gt;
      t.text :description&lt;br /&gt;
      t.string :status&lt;br /&gt;
      t.boolean :auto_signup&lt;br /&gt;
      t.references :assignment, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;20241029235713_create_suggestion_comments.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class CreateSuggestionComments &amp;lt; ActiveRecord::Migration[7.0]&lt;br /&gt;
  def change&lt;br /&gt;
    create_table :suggestion_comments do |t|&lt;br /&gt;
      t.text :comment&lt;br /&gt;
      t.references :suggestion, null: false, foreign_key: true&lt;br /&gt;
      t.references :user, null: false, foreign_key: true&lt;br /&gt;
&lt;br /&gt;
      t.timestamps&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The models and accompanying migrations form the basis of the reimplementation. The model files were updated to improve their attribute validation and fix some bugs with the model associations. The 'comments' field was removed from the 'Suggestion' model since the 'SuggestionComment' model exists. As a result of the migrations, 'schema.rb' is changed, but excluded from the above list since it is an auto-generated file.&lt;br /&gt;
&lt;br /&gt;
===Controller:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;routes.rb (Appended)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
resources :suggestions do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :add_comment&lt;br /&gt;
    post :approve&lt;br /&gt;
    post :reject&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;suggestions_controller.rb (Reimplemented)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old !! New&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class SuggestionController &amp;lt; ApplicationController&lt;br /&gt;
  include AuthorizationHelper&lt;br /&gt;
&lt;br /&gt;
  def action_allowed?&lt;br /&gt;
    case params[:action]&lt;br /&gt;
    when 'create', 'new', 'student_view', 'student_edit', 'update_suggestion', 'submit'&lt;br /&gt;
      current_user_has_student_privileges?&lt;br /&gt;
    else&lt;br /&gt;
      current_user_has_ta_privileges?&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    @suggestion_comment = SuggestionComment.new(vote: params[:suggestion_comment][:vote], comments: params[:suggestion_comment][:comments])&lt;br /&gt;
    @suggestion_comment.suggestion_id = params[:id]&lt;br /&gt;
    @suggestion_comment.commenter = session[:user].name&lt;br /&gt;
    if @suggestion_comment.save&lt;br /&gt;
      flash[:notice] = 'Your comment has been successfully added.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'There was an error in adding your comment.'&lt;br /&gt;
    end&lt;br /&gt;
    if current_user_has_student_privileges?&lt;br /&gt;
      redirect_to action: 'student_view', id: params[:id]&lt;br /&gt;
    else&lt;br /&gt;
      redirect_to action: 'show', id: params[:id]&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)&lt;br /&gt;
  verify method: :post, only: %i[destroy create update],&lt;br /&gt;
         redirect_to: { action: :list }&lt;br /&gt;
&lt;br /&gt;
  def list&lt;br /&gt;
    @suggestions = Suggestion.where(assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_view&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def student_edit&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def update_suggestion&lt;br /&gt;
    Suggestion.find(params[:id]).update_attributes(title: params[:suggestion][:title],&lt;br /&gt;
                                                   description: params[:suggestion][:description],&lt;br /&gt;
                                                   signup_preference: params[:suggestion][:signup_preference])&lt;br /&gt;
    redirect_to action: 'new', id: Suggestion.find(params[:id]).assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def new&lt;br /&gt;
    @suggestion = Suggestion.new&lt;br /&gt;
    session[:assignment_id] = params[:id]&lt;br /&gt;
    @suggestions = Suggestion.where(unityID: session[:user].name, assignment_id: params[:id])&lt;br /&gt;
    @assignment = Assignment.find(params[:id])&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    @suggestion = Suggestion.new(suggestion_params)&lt;br /&gt;
    @suggestion.assignment_id = session[:assignment_id]&lt;br /&gt;
    @assignment = Assignment.find(session[:assignment_id])&lt;br /&gt;
    @suggestion.status = 'Initiated'&lt;br /&gt;
    @suggestion.unityID = if params[:suggestion_anonymous].nil?&lt;br /&gt;
                            session[:user].name&lt;br /&gt;
                          else&lt;br /&gt;
                            ''&lt;br /&gt;
                          end&lt;br /&gt;
&lt;br /&gt;
    if @suggestion.save&lt;br /&gt;
      flash[:success] = 'Thank you for your suggestion!' unless @suggestion.unityID.empty?&lt;br /&gt;
      flash[:success] = 'You have submitted an anonymous suggestion. It will not show in the suggested topic table below.' if @suggestion.unityID.empty?&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'new', id: @suggestion.assignment_id&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def submit&lt;br /&gt;
    if !params[:add_comment].nil?&lt;br /&gt;
      add_comment&lt;br /&gt;
    elsif !params[:approve_suggestion].nil?&lt;br /&gt;
      approve_suggestion&lt;br /&gt;
    elsif !params[:reject_suggestion].nil?&lt;br /&gt;
      reject_suggestion&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # If the user submits a suggestion and gets it approved -&amp;gt; Send email&lt;br /&gt;
  # If user submits a suggestion anonymously and it gets approved -&amp;gt; DOES NOT get an email&lt;br /&gt;
  def send_email&lt;br /&gt;
    proposer = User.find_by(id: @user_id)&lt;br /&gt;
    if proposer&lt;br /&gt;
      teams_users = TeamsUser.where(team_id: @team_id)&lt;br /&gt;
      cc_mail_list = []&lt;br /&gt;
      teams_users.each do |teams_user|&lt;br /&gt;
        cc_mail_list &amp;lt;&amp;lt; User.find(teams_user.user_id).email if teams_user.user_id != proposer.id&lt;br /&gt;
      end&lt;br /&gt;
      Mailer.suggested_topic_approved_message(&lt;br /&gt;
        to: proposer.email,&lt;br /&gt;
        cc: cc_mail_list,&lt;br /&gt;
        subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
        body: {&lt;br /&gt;
          approved_topic_name: @suggestion.title,&lt;br /&gt;
          proposer: proposer.name&lt;br /&gt;
        }&lt;br /&gt;
      ).deliver_now!&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def notification&lt;br /&gt;
    if @suggestion.signup_preference == 'Y'&lt;br /&gt;
      if @team_id.nil?&lt;br /&gt;
        new_team = AssignmentTeam.create(name: 'Team_' + rand(10_000).to_s,&lt;br /&gt;
                                         parent_id: @signuptopic.assignment_id, type: 'AssignmentTeam')&lt;br /&gt;
        new_team.create_new_team(@user_id, @signuptopic)&lt;br /&gt;
      else&lt;br /&gt;
        if @topic_id.nil?&lt;br /&gt;
          # clean waitlists&lt;br /&gt;
          SignedUpTeam.where(team_id: @team_id, is_waitlisted: 1).destroy_all&lt;br /&gt;
          SignedUpTeam.create(topic_id: @signuptopic.id, team_id: @team_id, is_waitlisted: 0)&lt;br /&gt;
        else&lt;br /&gt;
          @signuptopic.private_to = @user_id&lt;br /&gt;
          @signuptopic.save&lt;br /&gt;
          # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
          send_email&lt;br /&gt;
        end&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      # if this team has topic, Expertiza will send an email (suggested_topic_approved_message) to this team&lt;br /&gt;
      send_email&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve_suggestion&lt;br /&gt;
    approve&lt;br /&gt;
    notification&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject_suggestion&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    if @suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
      flash[:notice] = 'The suggestion has been successfully rejected.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when rejecting the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
    redirect_to action: 'show', id: @suggestion&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def suggestion_params&lt;br /&gt;
    params.require(:suggestion).permit(:assignment_id, :title, :description,&lt;br /&gt;
                                       :status, :unityID, :signup_preference)&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    @user_id = User.find_by(name: @suggestion.unityID).try(:id)&lt;br /&gt;
    if @user_id&lt;br /&gt;
      @team_id = TeamsUser.team_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
      @topic_id = SignedUpTeam.topic_id(@suggestion.assignment_id, @user_id)&lt;br /&gt;
    end&lt;br /&gt;
    # After getting topic from user/team, get the suggestion&lt;br /&gt;
    @signuptopic = SignUpTopic.new_topic_from_suggestion(@suggestion)&lt;br /&gt;
    # Get success only if the signuptopic object was returned from its class&lt;br /&gt;
    if @signuptopic != 'failed'&lt;br /&gt;
      flash[:success] = 'The suggestion was successfully approved.'&lt;br /&gt;
    else&lt;br /&gt;
      flash[:error] = 'An error occurred when approving the suggestion.'&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Api::V1::SuggestionsController &amp;lt; ApplicationController&lt;br /&gt;
  include PrivilegeHelper&lt;br /&gt;
&lt;br /&gt;
  def add_comment&lt;br /&gt;
    render json: SuggestionComment.create!(&lt;br /&gt;
      comment: params[:comment],&lt;br /&gt;
      suggestion_id: params[:id],&lt;br /&gt;
      user_id: @current_user.id&lt;br /&gt;
    ), status: :ok&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def approve&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      transaction do&lt;br /&gt;
        @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
        @suggestion.update_attribute('status', 'Approved')&lt;br /&gt;
        create_topic_from_suggestion!&lt;br /&gt;
        unless @suggestion.user_id.nil?&lt;br /&gt;
          @suggester = User.find(@suggestion.user_id)&lt;br /&gt;
          sign_team_up_to_assignment_and_topic!&lt;br /&gt;
          send_notice_of_approval!&lt;br /&gt;
        end&lt;br /&gt;
        render json: @suggestion, status: :ok&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students cannot approve a suggestion.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def create&lt;br /&gt;
    render json: Suggestion.create!(&lt;br /&gt;
      title: params[:title],&lt;br /&gt;
      description: params[:description],&lt;br /&gt;
      status: 'Initialized',&lt;br /&gt;
      auto_signup: params[:auto_signup],&lt;br /&gt;
      assignment_id: params[:assignment_id],&lt;br /&gt;
      user_id: params[:suggestion_anonymous] ? nil : @current_user.id&lt;br /&gt;
    ), status: :ok&lt;br /&gt;
  rescue ActiveRecord::RecordInvalid =&amp;gt; e&lt;br /&gt;
    render json: e.record.errors, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def destroy&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      Suggestion.find(params[:id]).destroy!&lt;br /&gt;
      render json: {}, status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students do not have permission to delete suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  rescue ActiveRecord::RecordNotDestroyed =&amp;gt; e&lt;br /&gt;
    render json: e, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def index&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      render json: Suggestion.where(assignment_id: params[:id]), status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students do not have permission to view all suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def reject&lt;br /&gt;
    if PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      suggestion = Suggestion.find(params[:id])&lt;br /&gt;
      if suggestion.status == 'Initialized'&lt;br /&gt;
        suggestion.update_attribute('status', 'Rejected')&lt;br /&gt;
        render json: suggestion, status: :ok&lt;br /&gt;
      else&lt;br /&gt;
        render json: { error: 'Suggestion has already been approved or rejected.' }, status: :unprocessable_entity&lt;br /&gt;
      end&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students cannot reject a suggestion.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def show&lt;br /&gt;
    @suggestion = Suggestion.find(params[:id])&lt;br /&gt;
    puts @suggestion.user_id&lt;br /&gt;
    puts @current_user.id&lt;br /&gt;
    if @suggestion.user_id == @current_user.id || PrivilegeHelper.current_user_has_ta_privileges?&lt;br /&gt;
      render json: {&lt;br /&gt;
        suggestion: @suggestion,&lt;br /&gt;
        comments: SuggestionComment.where(suggestion_id: params[:id])&lt;br /&gt;
      }, status: :ok&lt;br /&gt;
    else&lt;br /&gt;
      render json: { error: 'Students can only view their own suggestions.' }, status: :forbidden&lt;br /&gt;
    end&lt;br /&gt;
  rescue ActiveRecord::RecordNotFound =&amp;gt; e&lt;br /&gt;
    render json: e, status: :not_found&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  private&lt;br /&gt;
&lt;br /&gt;
  def create_topic_from_suggestion!&lt;br /&gt;
    @signuptopic = SignUpTopic.create!(&lt;br /&gt;
      topic_identifier: &amp;quot;S#{Suggestion.where(assignment_id: @suggestion.assignment_id).count}&amp;quot;,&lt;br /&gt;
      topic_name: @suggestion.title,&lt;br /&gt;
      assignment_id: @suggestion.assignment_id,&lt;br /&gt;
      max_choosers: 1&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def send_notice_of_approval!&lt;br /&gt;
    Mailer.send_topic_approved_message(&lt;br /&gt;
      to: @suggester.email,&lt;br /&gt;
      cc: User.joins(:teams_users).where(teams_users: { team_id: @team.id }).where.not(id: @suggester.id).map(&amp;amp;:email),&lt;br /&gt;
      subject: &amp;quot;Suggested topic '#{@suggestion.title}' has been approved&amp;quot;,&lt;br /&gt;
      body: {&lt;br /&gt;
        approved_topic_name: @suggestion.title,&lt;br /&gt;
        suggester: @suggester.name&lt;br /&gt;
      }&lt;br /&gt;
    )&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def sign_team_up_to_assignment_and_topic!&lt;br /&gt;
    return unless @suggestion.auto_signup == true&lt;br /&gt;
&lt;br /&gt;
    @team = Team.where(assignment_id: @signuptopic.assignment_id).joins(:teams_user)&lt;br /&gt;
                .where(teams_user: { user_id: @suggester.id }).first&lt;br /&gt;
    if @team.nil?&lt;br /&gt;
      @team = Team.create!(assignment_id: @signuptopic.assignment_id)&lt;br /&gt;
      TeamsUser.create!(team_id: @team.id, user_id: @suggester.id)&lt;br /&gt;
    end&lt;br /&gt;
    if SignedUpTeam.exists?(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
      SignedUpTeam.where(team_id: @team.id, is_waitlisted: 1).destroy_all&lt;br /&gt;
      SignedUpTeam.create!(sign_up_topic_id: @signuptopic.id, team_id: @team.id, is_waitlisted: false)&lt;br /&gt;
    end&lt;br /&gt;
    @signuptopic.update_attribute(:private_to, @suggester.id)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The controller defines the behavior around suggestions and suggestion comments. It has been completely reimplemented to follow API convention and streamline the various endpoints that the frontend will interact with. Notable changes are:&lt;br /&gt;
&lt;br /&gt;
* Responses are in JSON instead of HTML&lt;br /&gt;
* Methods have proper error handling&lt;br /&gt;
* Public method names have been changed to standard Rails controller methods or method naming conventions&lt;br /&gt;
* Private method names describe their public side effects&lt;br /&gt;
* 'approve', 'notification', and 'send_email' are re-split into 'create_topic_from_suggestion!', 'send_notice_of_approval!', and 'sign_team_up_to_assignment_and_topic!' methods to improve logical code segmentation&lt;br /&gt;
* 'submit' method removed entirely as it is a &amp;quot;do-this-or-that&amp;quot; method; instead, the sub-actions are called directly&lt;br /&gt;
* The method that sends the email is simplified to a single call to the Mailer class and now uses a Rails query chain to reduce the number of database queries&lt;br /&gt;
* Each controller method uses a call to PrivilegeHelper instead of using the now deprecated 'allow_action?' method&lt;br /&gt;
&lt;br /&gt;
===Helpers:===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;display: flex; flex-wrap: wrap; gap: 10px;&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;mailer.rb (Added)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
class Mailer &amp;lt; ActionMailer::Base&lt;br /&gt;
  default from: 'expertiza.mailer@gmail.com'&lt;br /&gt;
&lt;br /&gt;
  def send_topic_approved_message(defn)&lt;br /&gt;
    @body = defn[:body]&lt;br /&gt;
    @topic_name = defn[:body][:approved_topic_name]&lt;br /&gt;
    @proposer = defn[:body][:proposer]&lt;br /&gt;
&lt;br /&gt;
    defn[:to] = 'expertiza.mailer@gmail.com' if Rails.env.development? || Rails.env.test?&lt;br /&gt;
    mail(subject: defn[:subject], to: defn[:to], bcc: defn[:cc]).deliver_now!&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot; style=&amp;quot;width: fit-content; overflow: auto;&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;div style=&amp;quot;font-weight: bold; line-height: 1.6;&amp;quot;&amp;gt;privilege_helper.rb (Copied)&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|+&lt;br /&gt;
! Old (authorization_helper.rb) !! New (privilege_helper.rb)&lt;br /&gt;
|- style=&amp;quot;vertical-align:top;&amp;quot;&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module AuthorizationHelper&lt;br /&gt;
  # Notes:&lt;br /&gt;
  # We use session directly instead of current_role_name and the like&lt;br /&gt;
  # Because helpers do not seem to have access to the methods defined in app/controllers/application_controller.rb&lt;br /&gt;
&lt;br /&gt;
  # PUBLIC METHODS&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; session[:user].role.has_all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
# ...&lt;br /&gt;
&lt;br /&gt;
  def current_user_and_role_exist?&lt;br /&gt;
    user_logged_in? &amp;amp;&amp;amp; !session[:user].role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt; || &amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot; line&amp;gt;&lt;br /&gt;
module PrivilegeHelper&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Super-Admin&lt;br /&gt;
  def self.current_user_has_super_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Super-Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Admin (or higher)&lt;br /&gt;
  def self.current_user_has_admin_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Administrator')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of an Instructor (or higher)&lt;br /&gt;
  def self.current_user_has_instructor_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Instructor')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a TA (or higher)&lt;br /&gt;
  def self.current_user_has_ta_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Teaching Assistant')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of a Student (or higher)&lt;br /&gt;
  def self.current_user_has_student_privileges?&lt;br /&gt;
    current_user_has_privileges_of?('Student')&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Determine if the currently logged-in user has the privileges of the given role name (or higher privileges)&lt;br /&gt;
  # Let the Role model define this logic for the sake of DRY&lt;br /&gt;
  # If there is no currently logged-in user simply return false&lt;br /&gt;
  def self.current_user_has_privileges_of?(role_name)&lt;br /&gt;
    current_user_and_role_exist? &amp;amp;&amp;amp; @current_user.role.all_privileges_of?(Role.find_by(name: role_name))&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  def self.current_user_and_role_exist?&lt;br /&gt;
    !@current_user.nil? &amp;amp;&amp;amp; !@current_user.role.nil?&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The helpers exist for the single responsibility principle, and to allow common features to be defined in one place and usable everywhere. The two helpers that are implemented are:&lt;br /&gt;
&lt;br /&gt;
* The mailer helper, which is responsible for sending emails&lt;br /&gt;
* The privilege helper, which helps determine the permissions level of the current user&lt;br /&gt;
&lt;br /&gt;
===Controller spec:===&lt;br /&gt;
&lt;br /&gt;
The controller spec file is part of the test plan and explained in the next section of this document.&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
Update suggestions_controller test file, to test the changed file, follow the change to api format, and use RSpec testing.&lt;br /&gt;
===Code coverage===&lt;br /&gt;
&lt;br /&gt;
1. Method: Show&lt;br /&gt;
 * Only shows when user is authorized to view the specified suggestion&lt;br /&gt;
 * Does not show when user is unauthorized&lt;br /&gt;
&lt;br /&gt;
2. Method: add_comment&lt;br /&gt;
 * adds a comment and then returns showing said comment&lt;br /&gt;
 * returns an error when comment creation fails&lt;br /&gt;
&lt;br /&gt;
3. Method: approve&lt;br /&gt;
 * approves the suggestion and shows updated suggestion if user is authorized to do so&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to approve suggestions&lt;br /&gt;
&lt;br /&gt;
4. Method: reject&lt;br /&gt;
 * rejects the suggestion and returns to suggestion when user is authorized&lt;br /&gt;
 * returns a forbidden error when user is unauthorized to reject suggestions&lt;br /&gt;
&lt;br /&gt;
5. Method: edit&lt;br /&gt;
 * edits suggestion and shows updated suggestion when authorized to edit the suggestion&lt;br /&gt;
 * returns forbidden error when user is authorized&lt;br /&gt;
&lt;br /&gt;
6. Method: create&lt;br /&gt;
 * creates suggestion if given suggestion data is valid, otherwise return an error&lt;br /&gt;
&lt;br /&gt;
7. Method: destroy&lt;br /&gt;
 * deletes the suggestion if user has the permissions to delete suggestions&lt;br /&gt;
&lt;br /&gt;
8. Method: index&lt;br /&gt;
 * show all suggestions if user has the privileges otherwise returns forbidden error&lt;br /&gt;
&lt;br /&gt;
9. Method: create_topic_from_suggestion&lt;br /&gt;
 * creates a suggestion using data from the existing suggestion and signs suggester up for topic&lt;br /&gt;
&lt;br /&gt;
10. Method: send notice of approval&lt;br /&gt;
 * sends information to mailer to send an email to suggestion creator that their suggestion was approved&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
* Add comments into the code to improve code documentation&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Piyush Prasad (pprasad3@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
* [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
* [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
* [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;br /&gt;
&lt;br /&gt;
==External Links==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/voltavidTony/reimplementation-back-end Project repo]&lt;br /&gt;
* [https://github.com/users/voltavidTony/projects/1 Project board]&lt;br /&gt;
* [# Pull request]&lt;br /&gt;
&lt;br /&gt;
==See Also==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2270._Improve_suggestion_controller Improve suggestion controller - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2022_-_E2250._Refactor_suggestion_controller.rb Refactor suggestion controller.rb - Fall 2022]&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016/E1643._Refactor_Suggestion_controller Refactor Suggestion controller - Fall 2016]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159185</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159185"/>
		<updated>2024-11-12T23:37:38Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Design Pattern==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Code coverage===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* Piyush Prasad (pprasad3@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* Anthony Spendlove (aspendl@ncsu.edu)&lt;br /&gt;
* Sean McLellan (spmclell@ncsu.edu)&lt;br /&gt;
* Dhananjay Raghu (draghu@ncsu.edu)&lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
# [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
# [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
# [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
# [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159174</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159174"/>
		<updated>2024-11-12T23:00:59Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Problem Statement */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes. Along with this, it needs to be updated to follow the new back end, which is being implemented using json and api paths.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Design Pattern==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Sequence Diagram==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Code coverage===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* &lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* &lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
# [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
# [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
# [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
# [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159173</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159173"/>
		<updated>2024-11-12T23:00:04Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: /* Problem Statement */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
The suggestion_controller.rb in Expertiza is responsible for managing all operations related to submitting and approving suggestions for new project topics proposed by students or teams. However, this controller currently violates the Single Responsibility Principle by directly interacting with members of other Model classes. To enhance maintainability and clarity, this functionality should be appropriately distributed among dedicated classes.&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Design Pattern==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Sequence Diagram==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Code coverage===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* &lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* &lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
# [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
# [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
# [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
# [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159159</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159159"/>
		<updated>2024-11-12T22:06:28Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Design Pattern==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Sequence Diagram==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Code coverage===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* &lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* &lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
# [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
# [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
# [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
# [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159158</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb (Design Document)</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb_(Design_Document)&amp;diff=159158"/>
		<updated>2024-11-12T22:06:12Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: Created page with &amp;quot;==Problem Statement==  ==Design Goal==    ==Design Pattern==    ==Class Diagram==   ==Sequence Diagram==   ==Solutions/Details of Changes Made==    ==Files Added/Modified==    ==== Changes to &amp;lt;code&amp;gt;app/models/teams_user.rb&amp;lt;/code&amp;gt; ====    ==== Changes to &amp;lt;code&amp;gt;app/models/team_user_node.rb&amp;lt;/code&amp;gt; ====   ==Test Plan==    ===Code coverage===    ==Next Steps==   ==Team==  ====Mentor====  *   ====Members====  *   ==References==  # [https://expertiza.ncsu.edu/ Expertiza] # [htt...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Problem Statement==&lt;br /&gt;
&lt;br /&gt;
==Design Goal==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Design Pattern==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Class Diagram==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Sequence Diagram==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Solutions/Details of Changes Made==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Files Added/Modified==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Changes to &amp;lt;code&amp;gt;app/models/teams_user.rb&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==== Changes to &amp;lt;code&amp;gt;app/models/team_user_node.rb&amp;lt;/code&amp;gt; ====&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Test Plan==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
===Code coverage===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Next Steps==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Team==&lt;br /&gt;
&lt;br /&gt;
====Mentor====&lt;br /&gt;
&lt;br /&gt;
* &lt;br /&gt;
&lt;br /&gt;
====Members====&lt;br /&gt;
&lt;br /&gt;
* &lt;br /&gt;
&lt;br /&gt;
==References==&lt;br /&gt;
&lt;br /&gt;
# [https://expertiza.ncsu.edu/ Expertiza]&lt;br /&gt;
# [https://docs.google.com/document/d/1fB9nHsop_yptjcj3CFn80BcZjXcSDbzcWwKvBDUTVrQ/edit?tab=t.0OSS Projects on Expertiza])&lt;br /&gt;
# [https://github.com/expertiza/expertiza Expertiza Github]&lt;br /&gt;
# [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza Wiki]&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb&amp;diff=159131</id>
		<title>CSC/ECE 517 Fall 2024 - E2477. Reimplement suggestion controller.rb</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2024_-_E2477._Reimplement_suggestion_controller.rb&amp;diff=159131"/>
		<updated>2024-11-12T20:05:04Z</updated>

		<summary type="html">&lt;p&gt;Spmclell: Created page with &amp;quot;===Project Description===  ===Problem Statement===  ===Objectives===  ===Development Strategy===  ===Project Design and Implementation===  ==== Functionality ====  ===Testing Plan===  ===Team Members===&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;===Project Description===&lt;br /&gt;
&lt;br /&gt;
===Problem Statement===&lt;br /&gt;
&lt;br /&gt;
===Objectives===&lt;br /&gt;
&lt;br /&gt;
===Development Strategy===&lt;br /&gt;
&lt;br /&gt;
===Project Design and Implementation===&lt;br /&gt;
&lt;br /&gt;
==== Functionality ====&lt;br /&gt;
&lt;br /&gt;
===Testing Plan===&lt;br /&gt;
&lt;br /&gt;
===Team Members===&lt;/div&gt;</summary>
		<author><name>Spmclell</name></author>
	</entry>
</feed>