This is a redo on this tutorial, which I had previously worked on before all my work got lost by a computer crash before committing (RIP).
ToDo --> add links to documentation when relevant
Using pipenv
instead of
virtualenv
for this project for a change of pace. Apparently it
combines a couple of
things.
To use, we hit pipenv shell
in the folder you care about and it will
automatically build pip files if it needs. You will need to install
files with pipenv install <package>
install of pip
. This will also
install the correct dependencies within your Pipfile
at the root
directory.
We'll need to install a few dependencies
pipenv install django djangorestframework django-rest-knox
Then we'll generate a new django project: django-admin startproject leadmanager
, followed by a leads
app with python manage.py startapp leads
and then add both to our settings file.
We'll create a model for a lead to use the ORM. These will be via
leads
app. To put these models into use, we'll need to create and
run migrations with python manage.py makemigrations leads
and
python manage.py migrate
to add it to our database. This will
include default tables Django uses.
To utilize django rest framework, we need to create serializers. These
will be used to expose access to our models. To make our life easier,
we'll use serializers.ModelSerializer
, which needs a Meta
subclass.
After the model is created, we'll add an api.py
. This will allow for
actual access to our model via the serializer. This will utilize
ViewSets, which are sort of like CBVs under Django.
Then we'll add in our URLs. Unlike in normal django, we'll use the router that is provided by django rest framework.
Order of Operations: Model => serializer => api => urls
We'll use Postman to do APIs. We can POST a new entry and GET all (at
the base root or /
) or get a specific post by specifiy ID (at
/<id>
).
As this is fully compliant REST, we can delete entries -- be sure to add the trailing slash!
This is a fully working API which is pretty neat.
Here we'll start to implement React into our app. Because we're
integrating it directly into Django, we will add it as its own app and
will not use any fancy scripts like create-react-app
(phew).
First, we'll start our app called frontend
which will hold all of
our react js work. After creating it, we'll need to add directories
for the app.
# tree view of `leadmanager/frontend`
.
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│ └── __init__.py
├── models.py
├── src
│ └── components
├── static
│ └── frontend
├── templates
│ └── frontend
├── tests.py
└── views.py
From the root directory, above django, we'll initialize our npm
project with npm init -y
and install webpack
and webpack-cli
in
addition to @babel/core
, babel-loader
, @babel/preset-env
,
@babel/peset-react
, and
babel-plugin-transform-class-properties
. Then we'll do react
libraries (see below).
# from my shell directly
$ npm i -D @babel/core babel-loader @babel/preset-env @babel/preset-react `babel-plugin-transform-class-properties`
$ npm i -D react react-dom prop-types
All these dependencies will pop up in our package.json
file.
To get using babel, we'll need a .babelrc
file to hold all of our
babel transforms (lints?).Then we'll create our webpack config file to
hold all of our webpack info inside of webpack.config.js
, which can
be separated out for different dev/qa/prod environments. Finally,
we'll make sure that package.json
knows the entry point for react
inside of the frontend
folder and output that to the static
folder
within frontend
. This will get the two environments separated as
well.
As is typical with React JS projects, we have an index.js
at the
root directory that is our entrypoint into the project while
components/App.js
really holds the bulk of our default homepage. As
for Django, an index.html
will be needed. We'll also pull down
bootstrap info from bootswatch and associated libraries.
Finally, we need to add the index load function in our frontend
and a URL to access it via urls.py
. This will necesitate having the
frontend urls being loaded as well. Also add frontend app to
leadmanager/settings.py
file.
Finally we can run everything!
Our homepage will consist of two components (to start):
Header
-- sits inside oflayouts
Dashboard
-- sits inside ofleads
True to React style, Dashboard
will be composed of two separate
components called Leads
and Form
, both within that folder. Doing
this will require using react's Fragment
class, importable at the
top level from react. These will be very simple for now -- we'll fill
them out later.
We'll implement redux here. This will allow us to have a golden source of state to store all of our information.
We'll start by installing a few packages from NPM:
$ npm i -D redux react-redux redux-thunk redux-devtools-extension
Typically, one starts by creating their store file for redux, which
holds all of our state. For making debugging easier, we'll use
redux-devtools-extensions
which makes the store.js
file a bit more
unique.
src/reducers
will then be created. An index.js
file is added as a
root file. For now, this will be virtually blank.
Going back to components/App.js
, we'll add the necesary code to
connect redux to our app.
If we pull up this app on our page, we can see that redux tools now appears. This extension is pretty neat as it allows us to inspect state while building the app!
Going back to our reducers/index.js
, let's create the necesary
imports then the necesary files for those imports to work.
Reducers evaluate actions and then send down the necesary state. They
evaluate types to do this. It's best practice to keep these in a
separate folder and file called actions
.
Our reducers object will export a function that takes in the current state and action. It then uses a switch function to evaluate which action happened and, once found, passes that down to the correct reducer for state change. State will change here typically here, and is returned from that exported function.
We include whatever is already in the state with a spread operator
...
and, if we specify conflicting keys, we can replace whatever was
in that state.
Once we have our reducer object done, we can go to action. It is here that our components will call actions (vis-a-vis functions), modify the state, and return the result.
For organizational purposes, the name for actions will be the same as in reducers.
Now we'll create a mapping file called leads.js
within actions that
does all the requesting. We'll add axios
to our npm. The first
function we add will be an exported function getLeads
and it will
return an object that gets modified.
Because we're using thunk
, as the request is async, we'll pass
through dispatch
as well. I presume the below notation passes
through dispatch before processing.
export const thing = () => dispatch => {
axios.get(....)
};
In our action, we want to dispatch the result to our reducer. This
involves calling dispatch
along with a type
and payload
. The
result is that type
w/e gets called and the passed payload
is
processed on our store.
Now we have our actions being propagated! We just need to connect this up to our component itself.
Going back to src/components/leads/Leads.js
, we'll add some imports
for redux. We'll also add PropTypes
from library prop-types
.
As a first step, we wrap our exported component with connect
. To
access our state, we need to map it to a component property. This is
done with an arrow function called mapStateToProps
. You can see this
inside of components/leads/Leads.js
. This is where propTypes
comes
in, as we can set those as well.
We can also pass in the getLeads
action within our connect function
to properly hook that up as well.
The end result -- looking only at the end of the file -- results in:
const mapStateToProps = state => ({
leads: state.leads.leads
});
export default connect(
mapStateToProps,
{ getLeads }
)(Leads);
You can see the state now reflected in redux tools. However, it's not
being called. To do that, we'll need to add it to
componentDidMount()
, a lifecycle function within React
components. Here, we can directly call the function.
Then, we'll rewrite render
. As we have a default state that is mapped
via this.props
we can call this.props.leads
as we mapped in
mapStateToProps
. By using Fragment
tags we can add further
components later or modify the existing one to include lists (I think
you need Fragment for this?). Note that each sub-jsx bit within our
rendered list will need a key, typically the id.
{ this.props.leads.map(lead => (
<tr key={lead.id}>
<td>{lead.id}</td>
<td>{lead.name}</td>
<td>{lead.email}</td>
<td>{lead.message}</td>
</tr>
)) }
The end result looks like:
Neat! Now we'll add a delete lead action and have that modify the corresponding lead. We'll create the action, then the type, then add in the necesary code on the reducer.
Note that this action will delete the lead on our local
computer. Instead, it will send the necesary request to delete it on
the server. This marks an important difference: we need ot have our
reducer take care of that by running filter
:
// reducers/leads.js
case DELETE_LEAD:
return {
...state,
leads: state.leads.filter(lead => lead.id !== action.payload)
}
One interesting aspect here is that when creating the button, we use
this.props.deleteLead.bind(this, lead.id)}
. Presumably this is to
ensure that our deleteLead
function is taking in the id(?)
We'll also add these connected functions as prop-types.
Now we'll add an addLead
form. This will start with the component in
components/leads/Forms.js
. First we give it an initial state, then
we pull out those sub pieces in the render function.
// within our render(), we can pull out specific values this way
const { name, email, message } = this.state;
Our Form component will have two aspects: one for changes and one for submissions.
The one for changes will simply change the values in question. This looks wonky in React JS:
onChange = e => this.setState({ [e.target.name]: e.target.value })
onSubmit
will call our action to submit a new lead. First we create
the action, then the type, then the reducer -- as always.
Using spread operators, we can write very compact and readable reducers as seen below:
case ADD_LEAD:
return {
...state,
leads: [...state.leads, action.payload]
};
Because we are not using properties for state in our Form
component,
we can pass in a null value when connecting the requisite
function. Don't forget to specify type for this function with
PropTypes
.
Now we'll add alerts.
To do this, we need to add the appropriate library for react js. We'll
install react-alert
, react-alert-template-basic
,
react-transition-group
. This allows us to pull in a function and
wrap our components with it, giving them the ability to call alerts.
Now we'll add our alert provider within the App.js
file. This will
be an app component that wraps everything (though we still want to
keep our storage provider as the outer most). We'll need to specify
options and a template. The former we'll create as alertOptions
and
the latter will be pulled from react-alert-template-basic
.
Next, we'll create a component for alerts. This will be a simple class
based component that returns almost nothing (just a fragment) but
calls an alert at load and is exported with withAlert(...)
. We'll
add if-else statements later on to get this well.
Then we'll need to create reducers that call this component when necesary.
The reducer will be simple, stored in reducers/errors.js
. For now,
just a GET_ERRORS
. Unlike the other actions, we'll simply dispatch
from the leads actions. They will call the requisite reducer action,
which will trigger an alert.
Let's go back to components/layout/Alerts.js
. Like before, let's
connect the state to our component. Instead of using
componentDidMount
, which gets called once at instantiation of the
component, let's use componentDidUpdate
. Because of the life cycles
of using React JS, this function will be better as it's called
multiple times.
componentDidUpdate
takes a variable called prevProps
. We'll use
this to check against the current state. If they've differed, then we
have problems.
Note to self: react-alert
had some major change from 4.x to 5.x. The
video uses the former while a blind install from npm will install the
former. The former will give weird esoteric errors I have not been
able to track down so keep that in mind.
I stopped at this point, he also installs a messages error handling which looked like overkill
PPS: I still don't get where the hell state.message and state.errors comes from given we never add it like that
Now we'll do token authenticaton (woo!)
This will start by going back to the backend in Django. We'll go to
our leads/model.py
and bring in the User model, default from
Django. Then we'll link the our Lead
model with our User
model
using owner=models.ForeignKey(User, ...)
.
After adding a models.ForeignKey
, we'll run makemigrations
and
migrate
.
Inside of leads/api.py
, we can change this up to accomodate our new
permission class. First, we need to change queryset
which, because
we only want to grab those owned by the user, we'll use a function.
def get_queryset(self):
return self.request.user.leads.all();
Lastly, we need to swap out the perform_create
function from its
default. This new function we define will take in a serializer
parameter and call serializer.save(owner=self.request.user)
so that
the corresponding owner of that lead will get saved.
There are a couple of things to do inside of settings.py
. First, a
new value will be added for REST_FRAMEWORK
so that it knows we want
to use Knox authentication. Next, we need to add in knox
to our
installed apps.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthencation',)
}
Following all this, we'll run a migration so knox can update itself.
Now we'll create an accounts app. This will be used for logging and registering new users via the REST API.
Each action will get it's own serializer. Remember that we create serializers followed by api (in this case no models needed).
For the RegisterSerializer
, we'll override the create
function and
add in some extra code to deal with validating the creation of a new
user. Know that User objects have their passwords salted, so this is
part of that process.
Then we need apis. These are in accounts/api.py
, which will use
generics where each function computes the name of the request
(ex. post()
handles POST requests). There is a slight change here in
that AuthToken.objects.create(user)
needs to be called manually.
Lastly, we'll add in urls.py
and punch in the appropriate bits,
adding into our master urls at leadmanager/urls.py
.
Note that when testing this you'll want to add the Authorization
header with Token <insert-token>
from the login, no quotes.
For our log out URL, we'll borrow from Knox via
knox_views.LogoutView.as_view()
. This will cause the token to be
invalidated.
We'll start by installing the npm package react-router
and
react-router-dom
. Then we'll bring it into React JS in our
src/components/App.js
file.
Because we want to keep the history, we'll want to bring in
HashRouter
(instead of BrowserRouter
, which relies on the HTTP
server underneath). On top of that, we'll also import Route
,
Switch
, and Redirect
.
Going down to the bottom, we'll wrap our components with our
Router
(starting at Fragment
).
Using Switch
, we can create our routes using Route
components. We'll tuck this under the container div tag.
<Switch>
<Route exact path="/" component={NameOfComponent}/>
// ... and so on
</Switch>
We'll get back to this in a minute. First, we'll create new routes for
logging in. These will be called Login.js
and Register.js
and will
sit under a new folder Accounts
.
This component will be a form. Like all good forms, it needs to watch
for changes (via onChange
) and submissions (via onSubmit
). Looking
at the change watching, we can give our input tags a prop onChange
and bind it (this proper react talk?) to our function
this.onChange
. For submissions, we'll do much the same with
onSubmit={this.onSubmit}
.
For reasons that are not quite clear to me, we'll be using arrow
functions for both onSubmit
and onChange
. For the former, we'll
first start by executing a e.preventDefault()
so that the form is
not submitted. For now, we'll do a console log though later we'll call
a login
action within the actions folder. onChange
will be a
simple this.setState
call to change the variable value.
Quickly building our Login
component, this will be more or less a
total copy of Register.js
except that we'll simplify it a bit.
Then we'll go back to App.js
and bring in those components for
routes. To make these accesible, we'll put these in our
layout/Header.js
component. Following this, you should be able to
follow the links on a reload.
Going back our components, we'll want to create a PrivateRoute
component. This will be in components/common
. This will be a
functional component that checks to see if the state says we're logged
in or not. Once we have that brought into App.js
, we can use it to
protect routes.
Next, we'll add our auth reducer. This will keep our authentication
keys within our redux state. On to the actions afterward. Like before,
we create our types, then match actions to them. By passing getState
along with dispatch
as objects into our function, we'll be able to
check for react state. We'll do this to check for any existing auth
tokens we may or may not have.
We'll start by adding new types to our actions/types.js
file.
Next, we'll add our action functions. These will be very similar to
our existing loadUser function. This will still check for the correct
header only this time we'll also pass in a body that is a stringified
json (e.g. JSON.stringify
). Once we create the action, we'll do the
same for our reducer.
Back to our component, we need to connect the two using connect
from
react-redux
. Then we'll call them in our onSubmit
function.
Before jumping into a Register
, we'll create logout
function and
component. This will involve types and such. Beacuse we only want some
links to be seen by the user, we'll make those dependent on being
logged in inside of our components/layout/Header.js
component.
We want to show an error if we struggle to log in. To do this, we'll
pull in our error messages and add it to the list of errors we
dispatch (mine is not currently dispatching an error, but the code to
check for it does exist in Alerts.js
.
Once we have this in, add tokenConfig(getState)
to all the functions
that need to be authenticated. Then, go into leads.js
and pass that
on as well as we'll need the auth token information to both get and
create new leads. Django should handle matching up the information on
its side.
Then end!