Approving and Rejecting Job Offers With React and FastAPI

UPDATED ON:

undraw svg category

Welcome to Part 20 of Up and Running with FastAPI. If you missed part 19, you can find it here .

This series is focused on building a full-stack application with the FastAPI framework. The app allows users to post requests to have their residence cleaned, and other users can select a cleaning project for a given hourly rate.

Up And Running With FastAPI

Previously, we implemented a system that allowed users to create offers for cleaning jobs they’re interested in. Those offers are stored in redux and are shown to users who have created them. However, there’s currently no way for the owner of a cleaning job to see and select from those offers. We’ll be creating a mechanism to do just that in this post.

First, we’re going to make some improvements to our cleanings redux slice.

Fetching All User-Owned Cleaning Jobs

Even though we have an adequate way of determining user ownership of a given cleaning resource, that information is transient. We have to wait until the user navigates to the cleaning job’s page, and when the user navigates away, we no longer have access to that data.

Let’s fix that by fetching all user-owned cleaning jobs as soon as the user is authenticated. We’ll store that data in redux and refactor our components to use our new structure.

Open up the redux/cleanings.js file and add the following:

redux/cleanings.js

Alright, let’s break this down.

We create 3 new action types and use them in our new fetchAllUserOwnedCleaningJobs action creator, as well as in our cleaningsReducer. Any GET request to the /api/cleanings/ endpoint returns a response containing all cleaning jobs that the currently authenticated user is the owner of. We take those jobs and index them under state.cleanings.data according to the id of each job. This is similar to how we handle updating our redux slice when we create a new cleaning job, only with multiple entries instead of a single one.

Now here comes the fun part.

We’re going to import this new action creator into our redux/auth.js file and dispatch it as soon as we fetch the authenticated user info from our server. Let’s also refactor our fetchUserFromToken action creator to use the apiClient abstraction that the rest of our action creators are using.

redux/auth.js

This refactor feels right for a few reasons.

First, there’s no longer a need to pass the access_token to our fetchUserFromToken action creator occasionally. Our apiClient service pulls the access token from localStorage regardless, so we can remove any unnecessary logic here.

We also have utilize the onSuccess callback of our apiClient as a clean way to dispatch the fetchAllUserOwnedCleaningJobs action creator as soon as the successfully authenticated user info is returned from our FastAPI backend. Now our cleanings slice is updated with user-owned cleaning jobs as soon as they log in.

Try it out. Login with a user that owns at least one cleaning job and check the state.cleanings.data. We should see our cleaning resources there. This is great, but there’s on slight problem.

Log out again and log in with a new user.

The cleaning jobs from the previously authenticated user are still there! That’s definitely not good.

Handle REQUEST_LOG_USER_OUT Action Type

One of the benefits of redux is that any reducer can listen to any action type and handle updates accordingly. We’re going to instruct each reducer to listen for the REQUEST_LOG_USER_OUT action type and reset their slice of state.

Start with the redux/cleanings.js file:

redux/cleanings.js

We import the REQUEST_LOG_USER_OUT action type from the redux/auth.js file and tell our cleaningsReducer to return initialState.cleanings whenever the user logs out. That should do a sufficient job of resetting state for our cleanings slice.

Now do the same for the redux/offers.js file:

redux/offers.js

Much better.

Go through the same flow now. Log in with a user that owns at least one cleaning job and then log out. Afterwards, authenticate with another user and make sure that none of those cleaning jobs are still stored in redux.

If all is right, we can now use our recent improvements to refactor how we determine user ownership in our CleaningJobsView component.

CleaningJobView.js

This is a clear case where the useSelector hook is an improvement over the connect higher order component. To replicate the same functionality with connect, we would need to pass the entire state.cleanings.data object to our CleaningJobView component and then use cleaning_id and user to select the correct attribute. Of course we could also use a package like reselect to do some filtering for us, but the current approach accomplishes our goal quite cleanly.

We can be confident that our new updates are working correctly, though it’s always nice to test them out. Navigate to cleaning jobs that the authenticated user does and doesn’t own, and ensure that the proper functionality is exhibited.

Check it out on Code Sandbox

phresh-frontend-part-8-fetching-all-user-owned-jobs

Go ahead and open back up the redux/offers.js file. We’re going to now make sure we can display a list of offers made for any cleaning job that the currently authenticated user owns.

Fetching All Offers For A Cleaning Job

As before, we’ll need 3 new action types and an action creator.

redux/offers.js

Our fetchAllOffersForCleaningJob action creator takes in a cleaning_id parameter and makes an HTTP GET request to the /cleanings/{cleaning_id}/offers/ endpoint. This endpoint should return an array of offers that have been made for this cleaning job, or an empty array if there are none.

We haven’t shown our reducers code here because we’re going to make a relatively substantial change to it. At the moment we already have an updateStateWithOfferForCleaning function that handles updating redux state for a single offer. However, it’s not designed to handle more than one offer, and using it multiple times for the same response seems inefficient. Let’s refactor our reducer to use a new function that accepts an array of offer objects.

redux/offers.js

Wow. That’s not the prettiest refactor, but it’ll get the job done. The updateStateWithOffersForCleaning function now takes in an array of offers and conditionally updates state depending on if there are any offers in the array. If there are, it grabs the cleaning_id attribute on the first one and then uses offers.reduce to compose an object where each offer is keyed by its user_id attribute. If there are no offesr in the array, then cleaningId will be undefined and the update will default to an empty object.

In our offersReducer, we call the updateStateWithOffersForCleaning function in three places, passing [action.data] when there is only a single offer returned from our FastAPI backend, and just action.data when the response is already an array.

There are other ways to accomplish the same goal here, but we have something functional at the moment, so let’s use it.

Back in our CleaningJobView component, make the following updates:

CleaningJobView.js

Now we’re talking.

We map the fetchAllOffersForCleaningJob function to our component props and call it in our useEffect hook only in the case that the user is the owner of this cleaning job. Again we employ the useSelector hook, this time to access all offers nested under the current cleaning job’s id.

As a simple check, we display the number of offers at the bottom of the page - if there are any. We’ll replace that with something more complete in a bit, but this will do for now.

Check it out on Code Sandbox

phresh-frontend-part-8-fetching-all-offers-for-a-cleaning-job

If we look in redux, we can see every offer made for a given cleaning job stored in our state tree and available for our frontend to use. Looking at those offers, we have the user_id available to us, but not any actual information about the user themselves. We’ll want to remedy that.

Before we move on to any more UI code, let’s switch gears for a moment and transition to our backend. We’re going to tweak a couple endpoints, add functionality to our repositories, and update a few of our essential libraries.

Upgrading Pydantic and FastAPI

We started building this application months ago, and a lot has changed in the FastAPI world since then. When we began FastAPI was on version v0.55.1. At the time this article was written, v0.62.0 was just released. That doesn’t seem like a big change, but some signficant improvements have been made to the framework. A simple look at the release history will provide some insight on this matter.

Over the same time, the pydantic library has also seen quite a few improvements. The updates from v.1.4 to the presently latest v1.7.3 can be seen here .

Changes resulting from these version upgrades are overall very positive and will help make our code a little bit easier to reason about. It will require some modifications to our pydantic models, but nothing that will break the way our application currently works.

Wanting to take full advantage of what the latest and greatest has to offer, we’re going to rebuild our docker container with the newest versions of both.

Open up the requirements.txt file and update it like so:

requirements.txt

Then go ahead and rebuild the container with docker-compose up --build.

Sit on that for a while and wait for it to finish.

Populating Offers

With that out of the way, our main task is to add a populate_offer method to our OffersRepository. We’ll use it to make sure that when we fetch offers for a single cleaning job, each offer is populated with the profile of the user who is making the offer.

Go ahead and open up the repositories/offers.py file.

repositories/offers.py

We’ve attached an instance of our UsersRepository to our OffersRepository and then leverage it in our populate_offer method that simply queries the user in question and populates our offer with the result.

Then in our list_offers_for_cleaning method, we add a populate parameter that determines whether or not the offers should include the full profile of the user who made the offer. Otherwise, we simply return the UserInDB model without any populated fields.

As always when we make any substantial changes to the backend, run the test suite to make sure nothing is broken.

Fortunately, this time around things seem to be in order! With all the tests still passing, we can be relatively confident that no additional changes need to be made to support our refactor.

Let’s now put our new updates to good use. Back in the frontend, we’ll develop an interface that allows the owner of a cleaning job to select from any of the offers and accept one they like.

Accepting and Rejecting Offers

The first thing we’re going to do is create a new component to host the list of offers made for a given cleaning job. We’ll call it CleaningJobOffersTable.js.

And we’re going to build this component using an elastic-ui elements that we haven’t seen before - EuiBasicTable.

CleaningJobOffersTable.js

One of the best components offered by elastic-ui - EuiBasicTable is an opinionated high level component that standardizes both display and injection. At its most simple it only accepts two properties:

Our component accepts an array of offers and passes them to the EuiBasicTable as the items prop. We define a columns array containing objects that determine how each item should be displayed. We’re choosing to display 4 columns:

The itemId prop of EuiBasicTable indicates what item attribute should be used as a unique identifier. We also specify a custom EuiEmptyPrompt when no offers are present. We pass that component as the message prop to our table.

The custom action we define is an Accept Offer button that the owner can use to decide to select an offer from a given user.

Make sure to export the component as well.

components/index.js

For each cleaning job page, we’ll show our offers table below the CleaningJobCard.

Open up the CleaningJobView.js component and update it like so:

CleaningJobView.js

After importing our newly-minted CleaningJobOffersTable component, we provide it with any offers for the current cleaning job and render it at the root path only if the user is the owner of the cleaning job.

If we navigate to the page of a cleaning job with pending offers, we should see them displayed nicely in an EuiBasicTable.

phresh offers for cleaning table

All that’s left to do is make sure that the Accept Offer button is fully functional.

Open up the redux/offers.js file and update it one more time.

redux/offers.js

Alright. More of the same going on here.

3 new action types, an acceptUsersOfferForCleaningJob action creator, and state updates in our reducer for each action type. We use the onSuccess callback in our action creator to fetch all the offers for the current cleaning job as soon as the user has selected one. This way, our state will be updated with offers showing the correct status.

Time to put it them to good use.

CleaningJobView.js

And just like that, we’re in business!

Give it a whirl. Navigate to that same page and click the Accept Offer button.

If all goes well, the UI should be updated after a short delay, and we should see the new state represented in our CleaningJobOffersTable component. Now log out and authenticate with a user who made one of the offers. The beta badge on the CleaningJobCard should also display either OFFER ACCEPTED or OFFER REJECTED depending on the user.

Check it out on Code Sandbox

phresh-frontend-part-8-accepting-and-rejecting-offers

Amazing.

Wrapping Up And Resources

We accomplished quite a bit in this post, with the majority of new code being added to our frontend repo. Our offers redux file has filled out significantly, and we refactored our cleanings slice to fetch all user owned cleaning jobs as soon as they’re authenticated. On top of that, we made sure to purge redux of all sensitive data whenever a user logs out.

At the same time, we upgraded our FastAPI and pydantic libraries to their latest versions, refactored some of our models, and implemented the ability to populate offers with their respective owners. Afterwards, we fixed a few tests that were broken in the process.

Propsective cleaners can now create offers for cleaning jobs they’re interested in, and the owners of those jobs can choose which offer to accept/reject. On the offers front, all that’s left is implementing rescinding and cancelling offers functionality.

Before we go there, we’re going to implement a “feed” page. We’ll need to design a layout for prospective cleaners, showing them available jobs and any updates that we deem relevant.

Github Repo

All code up to this point can be found here:

Tags:

Previous Post undraw svg category

Creating and Viewing Job Offers With React and FastAPI

Next Post undraw svg category

Serving A Paginated Activity Feed From FastAPI