React Fundamentals in Django Job Hunt App

Long ago, while I was at CalPERS, I got to explore some with React.  I went through two minor tutorials, just to then be exposed to an employee’s implementation of a large React App (the only place I’ve seen React used at CalPERS is their Board Agenda “Insight” app). Admittedly, I was slightly overwhelmed.

On my Laravel – React Recruitment Tracker, the handful of React Code snippets given in the Laravel Bootcamp Inertia with React was enough to help review some of the basics that I had learned years prior.  However, the scope of that application had no real re-use within the application itself and the feature set was so limited that I did not have to dig deep into React to understand what I needed to have something to demonstrate.

Today, I am releasing my latest code that demonstrates many different React Fundamentals. Check out the code in the master / production branch:

https://github.com/bbornino/django_react_job_search

Learning React

Before really developing the Django REST React application, I spent quite a lot of time going through the React Dev Documentation. That site’s tutorial documentation and the Django REST framework applications I had done a month prior, were really enough to get me to understand the depth of what could be done with React. From there, I am very much a learn-by-doing (and some trial and error) engineer.  I learned much more about the ins and outs of React programming, both using the class and functional methods of React components. I hope to return to that site, sooner than later, and implement more of the advanced React features that it described, including reducers, advanced event handlers, references, effects, and custom hooks.

New Job Application Tracker Releases

Classically, I come from a “waterfall” background.  In the world of Electronics, chip design, and mainboard development, there is no room for an agile sprint.  However, once I was firmly in Software Engineering land as a Developer (PeopleSoft was more of a sustaining support than raw development), I’ve been doing Agile Methodology, complete with daily standup (scrum meetings) and sprints (short incremental releases with all stakeholders involved at every step).

Though I don’t have those other stakeholders involved in this particular app, I am still making the effort to keep the reader (you) in mind as I develop, document, and release. So, you’ll find in the repository now TWO branches: development and master.  My development efforts are all being checked into the development branch.  Now that it is time for another “release” (complete with release history update, deployment/merge to the production branch, Blog entry, and updates to the website page), I’m hoping to have a similar release process.

Yes, there are a bunch of minor tweaks, but here are the bigger items that showed up, and some of my thoughts on the implementation…

Job Sites and Job Postings Added

The initial release (sprint) focused on the primary components and framework of a smaller piece of the application: the opportunities that come through from recruiters directly to me.  The “more complicated” piece (not really, but it has a database relational relationship) is the Job Sites and the corresponding Job Postings.  This release uses those primary components developed before.  For details on the application pages itself, see the application page.  Here, I’m going through the technical code aspects.

I think that the most notable item that I learned through this release is the power of React.  I was able to easily use the components from the opportunity page to a point where there was virtually no effort.  In the past, I would have put some of those pieces in their own HTML and JavaScript file and done an HTML import of the page.  Then I would have to intertwine the JavaScript variables getting imported into multiple calling pages and… but the React integration of reusable components made none of this experience feel like a Frankenstein effort.

Job Site Changes

Job Sites were created super fast thanks to the reusable components created from the Opportunities sprint. Pulling in the components created for the Data Table and the CKEditor5, this page was a snap. The only new item that I had not done yet were the check boxes which were super easy to implement. The interesting challenge and change was to create the Job Site View page, which used some key learnings from REST to know how to create the new serializers and Django methods. In fact, this portion of the application felt like the only real Python and Django work I have done on this project (everything else feels like 98% React!). Is it bad to admit that I felt like I was cheating, using AI to figure out how to develop those blocks? Yes, AI screwed up a bunch of times, but it felt like we were learning together the terminology, features, and how best to implement the application. I look forward to my next sprint when I’ll be learning how to use Postman for developing a REST API.

Job Posting Changes

This application started with the Ruby on Rails Application Features. From there, I had lots of exciting (to me) enhancements, including new fields. Some of these new fields are:

  • Related Job Site Selection
  • Recruitment Stage
  • Comments
  • Posting Site Domain and Password
  • Outreach Names (for all of the people I tried to contact)
  • Time Taken to work on the application (to help roll up how much time I’m actually spending)
  • Job Scan Percentage (to monitor how close to what the ATS is looking for)

See the updated project page for those photos.

Reports

In addition to the Job Sites and Job Postings, the other key feature was reports. One of the fundamental report features that was missing from the Ruby on Rails Application that I’ve done in past PHP Applications is a REST-ready report structure. The menu and front-end images are available on the main application page. I’ve always used this type of URL on the front end:

{/* Reports Submenu */}
<UncontrolledDropdown nav inNavbar>
  <DropdownToggle nav caret>Reports</DropdownToggle>
  <DropdownMenu end>
    <DropdownItem tag={Link} to="/reports/postingsAppliedSince">Postings Applied</DropdownItem>
    <DropdownItem tag={Link} to="/reports/perSite">Per Site</DropdownItem>
    <DropdownItem tag={Link} to="/reports/perWeek">Per Week</DropdownItem>
  </DropdownMenu>
</UncontrolledDropdown>

On the back end, there is a single report function. The function receives the report type (name) and the optional start date. They all return the same exact data structure of a JSON containing the report name, column info, and report data. Using the same exact structured JSON enabled one Report View, or in the React case: one Report Component that can generically draw any kind of report that I return.

One of the common interview questions that a full stack (and back end) engineer tends to be asked: rate your SQL experience. My response is always that depends on who is asking. DBAs and very senior data people will often use advanced tools (like an EXPLAIN) and SQL procedures. Those are not typical, everyday functions of a back-end developer. Pretty much everything else, I’ve done. Sure, I can easily do Object-Relational Mapping (ORM). But as a developer, when you need to develop the query in the SQL tool, and it has several joins, dynamically created tables, date field formatting, a group by, and a count… it is easier to copy and paste the raw SQL query into the model instead of building some complex ORM statement that takes up 5 times as many lines.

@api_view(['GET'])
def job_postings_report(request, report_type, reference_date=None):
    
    valid_report_types = ["postingsAppliedSince", "perSite", "perWeek"]
    if report_type not in valid_report_types:
        return Response(
            {"error": "Invalid report type. Valid types are: postingsAppliedSince, perSite, perWeek."},
            status=status.HTTP_400_BAD_REQUEST
        )

    # Initialize the reference date variable
    ref_date = None
    formatted_date = None
    # Handle reference date conversion
    if reference_date:
        try:
            ref_date = timezone.datetime.fromisoformat(reference_date)
            # formatted_date = ref_date.strftime('%B %d, %Y')     # easier to read.  Takes lots of space.
            formatted_date = ref_date.strftime('%m/%d/%y')
        except ValueError:
            return Response({"error": "Invalid date format."}, status=status.HTTP_400_BAD_REQUEST)
        
    # Initialize report metadata
    report_name = ""
    report_fields = []
    report_data = []

    # Determine which report to run
    if report_type == 'postingsAppliedSince':
        report_name = f"Postings Applied Since {formatted_date}" if reference_date else "Postings Applied Since"
        report_fields = [
            {"field_title": "Company Name", "field_name": "company_name", "sortable": True},
            {"field_title": "Posting Title", "field_name": "posting_title", "sortable": True},
            {"field_title": "Posting Status", "field_name": "posting_status", "sortable": True},
            {"field_title": "Applied At", "field_name": "applied_at", "sortable": True},
            {"field_title": "Rejected After Stage", "field_name": "rejected_after_stage", "sortable": True}
        ]

        sql_query = """
            SELECT p.id, p.company_name, p.posting_title, p.posting_status, p.applied_at, p.rejected_after_stage, s.site_name, s.id as site_id
            FROM job_search_jobposting p
            JOIN job_search_jobsite s ON p.job_site_id_id = s.id
            WHERE p.applied_at >= %s
        """
        with connection.cursor() as cursor:
            cursor.execute(sql_query, [reference_date or '2024-01-01'])
            report_data = dictfetchall(cursor)

    elif report_type == 'perWeek':
        report_name = f"Applications Per Week {formatted_date}" if reference_date else "Applications Per Week"
        report_fields = [
            {"field_title": "Year", "field_name": "year", "sortable": False},
            {"field_title": "Month", "field_name": "month", "sortable": False},
            {"field_title": "Week", "field_name": "week", "sortable": False},
            {"field_title": "Week Count", "field_name": "week_count", "sortable": False}
        ]


        # Basic SQL query without date formatting
        sql_query = """
            SELECT 
                p.applied_at AS applied_at
            FROM job_search_jobposting p
            WHERE p.applied_at >= %s
            ORDER BY p.applied_at ASC
        """

        with connection.cursor() as cursor:
            cursor.execute(sql_query, [reference_date or '2024-01-01'])
            raw_data = dictfetchall(cursor)
            
        # Process the data in Python and format dates
        report_data = []
        week_count = {}

        for row in raw_data:
            # Convert applied_at to Python datetime object
            applied_at_dt = row['applied_at']

            # Extract the year, week number, and month from the date
            year = applied_at_dt.strftime("%Y")
            week = applied_at_dt.strftime("%W")
            month = applied_at_dt.strftime("%B")

            # Create a unique key for each year and week combination
            year_week_key = (year, week)

            # Update the week count for each unique year-week
            if year_week_key not in week_count:
                week_count[year_week_key] = 0
            week_count[year_week_key] += 1

        # Add the data to the report data
        for (year, week), count in week_count.items():
            report_data.append({
                "year": year,
                "week": week,
                "month": month,  # Since it's weekly, month is less relevant, but can be included
                "week_count": count
            })

    elif report_type == 'perSite':
        report_name = f"Postings Per Site {formatted_date}" if reference_date else "Postings Per Site"
        report_fields = [
            {"field_title": "Site Name", "field_name": "site_name", "sortable": True},
            {"field_title": "Site Count", "field_name": "site_count", "sortable": True}
        ]

        sql_query = """
            SELECT s.site_name, COUNT(p.id) AS site_count
            FROM job_search_jobposting p
            JOIN job_search_jobsite s ON p.job_site_id_id = s.id
            WHERE p.applied_at >= %s
            GROUP BY s.site_name
            ORDER BY site_count DESC
        """
        with connection.cursor() as cursor:
            cursor.execute(sql_query, [reference_date or '2024-01-01'])
            report_data = dictfetchall(cursor)

    else:
        return Response({"error": "Invalid date format."}, status=status.HTTP_400_BAD_REQUEST)

    # Create a response dictionary
    response_data = {
        "report_name": report_name,
        "report_fields": report_fields,
        "report_data": report_data
    }

    # Serialize the response using ReportJobPostingSerializer
    serializer = ReportJobPostingSerializer(data=response_data)
    if serializer.is_valid():
        return Response(serializer.validated_data)
    return Response(response_data, status=status.HTTP_200_OK)


# Helper function to convert cursor data to a dictionary
def dictfetchall(cursor):
    "Return all rows from a cursor as a dictionary"
    columns = [col[0] for col in cursor.description]
    return [dict(zip(columns, row)) for row in cursor.fetchall()]

Data Table Filters

To an experienced React Programmer, the data table filters may have been nothing.  Since I was just starting out, it felt a little intimidating.  The old style, non-react-based page, datatables.js, everything is rolled up into one package. That is simply not the case with the React component that I was able to find.  I spent a rather exhaustive amount of time in July 2024 finding an implementation that had any kind of “built-in” data table support for sorting, filtering, and CSV download.  Alas, this one is the closest that I was able to find. 

Datatables.js automatically builds in the input box and automatically searches the entire table (rows and columns).  This component does not.  I had to create the input box and clear button.  None of this belongs in the data table component that I created: the filter functionality belongs in the calling component (the initially intimidating part).  Which, in hindsight, makes sense: the filter is specific to a particular column.  The filter doesn’t care about the table structure itself. Here is how I implemented the filter code for the react component:

filterJobPostingsByParams = (companyName, postingTitle) => {
  const { jobPostings } = this.state;
  const filteredItems = this.state.jobPostings.filter(item => 
            item.company_name && 
            item.company_name.toLowerCase().includes(companyName.toLowerCase()) && 
            item.posting_title && 
            item.posting_title.toLowerCase().includes(postingTitle.toLowerCase()))

  this.setState({ filterCompanyNameText: companyName, 
                  filterPostingTitleText: postingTitle,
                  filteredJobPostings: filteredItems})
};

onCompanyNameFilter = e => {
  this.filterJobPostingsByParams(e.target.value, this.state.filterPostingTitleText)
}

onCompanyNameClear = () => {
  this.filterJobPostingsByParams('', this.state.filterPostingTitleText)
}

onPostingTitleFilter = e => {
  this.filterJobPostingsByParams(this.state.filterCompanyNameText, e.target.value)
}

onPostingTitleClear = () => {
  this.filterJobPostingsByParams(this.state.filterCompanyNameText, '')
}

Here are the React Components used to draw out the table with its filter fields:

<Container>
  <Row className="m-4 align-items-center">
    <Col md="3"><h1>All Job Postings</h1></Col>
    <Col lg="3" md="6">
      <Link to='/job-posting-new'>
        <Button color="success" > Create Job Posting</Button>
      </Link>
    </Col>
    <Col lg="3" md="3">
      <InputGroup>
        <Input id="search" type="text" 
               className="m-0"
               placeholder="Filter by Company Name" 
               aria-label="Search Input"
               value={this.state.filterCompanyNameText}
               onChange={this.onCompanyNameFilter} />
        <Button color="danger" 
                onClick={this.onCompanyNameClear}
                className="">X</Button>
      </InputGroup>
    </Col>
    <Col lg="3" md="3">
      <InputGroup>
        <Input  id="search" type="text" 
                className="m-0"
                placeholder="Filter by Posting Title" 
                aria-label="Search Input"
                value={this.state.filterPostingTitleText}
                onChange={this.onPostingTitleFilter} />
        <Button color="danger" onClick={this.onPostingTitleClear}  
                className="">X</Button>
      </InputGroup>
    </Col>
  </Row>
    <DataTableBase columns={this.columns}
                   data={this.state.filteredJobPostings}
                   paginationPerPage={100}
                   onRowClicked={this.onRowClicked} />
</Container>

The final React Data Table Component that is imported by multiple pages:

import React from 'react';
import DataTable from 'react-data-table-component';

const selectProps = { indeterminate: isIndeterminate => isIndeterminate };
const paginationComponentOptions = {
	selectAllRowsItem: true,
	selectAllRowsItemText: 'ALL',
  };

function DataTableBase(props) {
  const emptyTableMessage = (props.data.length) ? "Loading Data" : "No Table Data";

  return (
    <DataTable
	selectableRowsComponentProps={selectProps}
	paginationComponentOptions={paginationComponentOptions}
	noDataComponent={emptyTableMessage}
	paginationPerPage={25}
	paginationRowsPerPageOptions={[10,25,100]}
	pagination striped highlightOnHover dense
	{...props}
    />
  );
}

export default DataTableBase;

Other Notables

Yes, there were plenty of tiny changes which only took a minute or five here or there.  But these were the handful of noteworthy improvements (which don’t need an exhaustive explanation):

Utility Functions 

You never really know if that particular function you write will be necessary only for THIS component, or other components.  Turns out, those formatting date functions were being used everywhere:

export const formatInputFieldDateTime = (originalDateTime) => {
    if (originalDateTime === null) return null;
    // Properly format date-time
    // Going into the database, the time zone is saved.
    // The front end widget does not need a time zone, nor the 'T'.
    // From: 2024-09-02T14:19:00-07:00
    // To: 2024-09-02 14:19:00   (no time zones)
    var originalDtArr = originalDateTime.split('T')
    var originalDtTimeArr = originalDtArr[1].split('-')
    return originalDtArr[0] + ' ' + originalDtTimeArr[0]
}

export const formatDisplayDateTime = (rawDate) => {
    if (rawDate === null) return null;
    // From a code perspective, I would love to live with the default:
    // const theDate = new Date(rawDate).toLocaleString('en-US')
    // 9/5/2024, 5:42:00 PM
    // but... you can't easily turn off the seconds!
    // return: Tuesday, Sep 10, 2024, 5:42 AM
    const theDate = new Date(rawDate).toLocaleString('en-US', {
        weekday: 'long',
        year: 'numeric',
        month: 'short',
        day: 'numeric',
        hour12:true,
        hour:'numeric',
        minute:'numeric'})
    return theDate
}

// return: Sep 10, 2024
export const formatDisplayDate = (rawDate) => {
    if (rawDate === undefined || rawDate === null) return null;
    
    const theDate = new Date(rawDate).toLocaleString('en-US', {
        year: 'numeric',
        month: 'short',
        day: 'numeric'})
    return theDate
}

Static Page Menu Consolidation

Over time, more static pages will be added to this app.  Right now, there are the About (with specs), Job Hunt Tips, and Release History static pages.  I also know that I’ll also need to add a User Guide (more in-depth than just the features) and Boolean Search. And more than likely, Public Services to be aware of (at least for here in California).  Everything was moved into one consolidated sub-menu:

{/* Static Pages Submenu */}
<UncontrolledDropdown nav inNavbar>
  <DropdownToggle nav caret>Information</DropdownToggle>
  <DropdownMenu end>
    <DropdownItem tag={Link} to="/about">About</DropdownItem>
    <DropdownItem tag={Link} to="/about">User Guide</DropdownItem>
    <DropdownItem divider />
    <DropdownItem tag={Link} to="/job-hunt-tips">Job Hunt Tips</DropdownItem>
    <DropdownItem tag={Link} to="/about">Boolean Search</DropdownItem>
    <DropdownItem divider />
    <DropdownItem tag={Link} to="/release-history">Release History</DropdownItem>
  </DropdownMenu>
</UncontrolledDropdown>

Posting Job Site Selection

One of the annoying parts of the Ruby on Rails version: I could not easily change a Job Posting’s site (fine: I did not put much effort behind figuring out a non-critical feature). I would have been on one site, exhausted what I could find, and mentally exhausted, then I’d move over to another site… but I would forget to change which job site the application was displaying, and… dang it: that Dice.Com posting I really just found on Linked In. The componentDidMount function calls both the getJobPosting and getJobSites:

componentDidMount() {
  const pathArr = window.location.pathname.split('/')
  if (pathArr[1] === "job-posting-new") {
    // Set the applied at date time to now, in correct format
    var currentdate = new Date().toLocaleDateString('en-CA')
    var currenttime = new Date().toLocaleTimeString('en-US',
                 { hour12: false, hour: "numeric", minute: "numeric"})

    // use value to set the job_site_id IF recieved a job_site_id
    if(pathArr[2]) {
      this.setState({job_site_id:pathArr[2],
                     applied_at: currentdate + 'T' + currenttime})
    }
  } else {
    // NOT new.  Use as the posting id
    this.setState({job_posting_id:pathArr[2]});
    this.getJobPosting(pathArr[2]);
  }

  this.getJobSites();
};

getJobSites = e => {
  axios.get(JOB_SITE_API_URL).then( res => {
            this.setState({job_sites:res.data})
  });
}

Then in the rendered component, I returned the input field:

<FormGroup>
  <Label for="job_site_id">Posting Job Site Source</Label>
  <Input
    type="select" required
    id="job_site_id"
    name="job_site_id"
    onChange={this.onChange}
    value={this.state.job_site_id}>
     <option value="">Select Job Site</option>
     {this.state.job_sites.map((option) => (
       <option key={option.id} value={option.id}>
               {option.site_name}</option>
     ))}
  </Input>
</FormGroup>

I’m hoping that, now that I have the remaining features from the Ruby on Rails Application, future releases will be much more frequent and smaller. Such as:

  • Dynamic Dashboard Statistics with REST documentation developed using POSTman
  • User accounts and authentication and other security features using the OWASP Top 10
  • File Attachments, saving encrypted files in AWS S3, and non-relational databases
  • Docker Containers then deploying to AWS

Till then, enjoy browsing the code repository!