SPA Code Walk Through
Here are the additional resources referenced in the lesson:
00:00
In the previous lesson, I showed you an example of an SPA architecture for the sample Sched
application. In this lesson, I’m going to dig in deeply and show you some of the code that goes along with it. Even for a relatively small application, there’s a fair number of moving parts here.
00:17 There’s HTML, there’s Python, there’s JavaScript, there’s Vue.js-specific JavaScript, and Vue.js-specific templates. I’m not going to go through all of that. For the most part, I’m going to concentrate on the server-side Python and some of the JavaScript. In particular, I want to highlight the parts where the DRF is interacted with and then some integral parts of the JavaScript inside of the view pieces on the client side.
00:45 The site you’re watching this on, of course, isn’t called Real JavaScript—it’s called Real Python—so if you’re not familiar with JavaScript, this could be a little bit painful. The expectation here isn’t for you to understand every little bit and piece, but just to get a high-level view of how the parts fit together.
01:01 Don’t forget all of this is available inside of the supplementary materials, and you can play with it on your own if it helps you understand.
01:09
If you’re going to run the sample code for yourself, you’re going to need a couple things. First off, you’ll need to install the dependencies. The dependencies are listed inside of requirements.txt
.
01:20
In order to keep your Python environment clean, it’s best practice to use a virtual environment for this. You can install all of the requirements.txt
file by using the -r
parameter to the pip install
command.
01:33 This’ll take everything inside of the text file and install all of it.
01:39
Inside of the Sched/
directory, you’ll find a shell script that you can use to create the database, do the migrations, and set everything up.
01:47 This is a Bash shell script. It will only work if you are using some sort of Unix derivative, so if you’re on a Mac, on Linux, or you’re using the Windows Linux subsystem, then you can use this command as a shortcut.
02:01 Here’s the contents of that script. If you happen to be running on regular Windows and you can’t use this script, then type these six commands in and you’ll be good to go.
02:12
Actually, you’ll only need the last four, because if you’ve got a brand new environment, there won’t be any PYC files or a SQLite file to get rid of. The first command, wipe_migrations
, comes out of django-awl
. It destroys any migration files.
02:28 I use this command when I’m playing with a brand new project so that I don’t create overly complicated chains of migration history when I don’t really care about the migration history because I haven’t gone into production yet. So it starts me clean each time.
02:42
Then makemigrations
and migrate
will set up and run your database, and then loaddata
is used to grab the sample data for the timelines.
02:50 The first half of this script gets rid of anything that’s there already, so if you play around and you want to start from scratch, run the script again.
03:00
Let me show you what this application looks like in the browser before I show you the code. Offscreen, I’m running the Django dev server, and here I’m going to hit the homepage of localhost:8000
.
03:11 Here’s the first of my two pages of my single-page application. This one is a standard Django view that spits out a simple Bootstrap listing of the timelines that are in the database.
03:23 Clicking on Peru takes me into the second page of my single-page application, and this one’s a little more SPA-ish. This is built with Vue.js. Each row in the schedule corresponds to an item in the database. All of the rows are being sent down via JSON and then interpreted by Vue.js.
03:46 I can click the Add button and get a popup for entering new information.
03:54 This popup is one of the Bootstrap modal popups coming from that library that I told you about. The popup is REST-aware. This form inherits from a base class that knows how to create things in REST.
04:07
It uses the names of the fields inside of the form and populates an AJAX call up to the server with a POST
method. By doing this, I need very little code to be able to create new objects on the server.
04:21
I just need the form to create them and to make sure that the name
attributes of the form are properly set.
04:28 Submitting the form does a call to the server and updates what’s on the screen.
04:33
Clicking on one of these objects pops up the same form. Now the same form can be used to do an update. The bsmodals
library takes care of all of this for you.
04:50
Clicking the Delete button brings up a confirmation dialog. These dialogs are also part of bsmodals
. If I click Yes, then a JavaScript function is called, calling this REST DELETE
on the object in question.
05:04 I’ll show you the code to that shortly. And finally, this application is drag-and-drop-enabled. The handles on the left-hand side of the screen allow me to change the order of the items.
05:19
This no longer follows the movie, but you see what I’m talking about. The movement updates the rank of the item, affecting the order on the server, and this has to be done with a custom action in order to take advantage of how RankedModel
work. Here are the models for the application.
05:38
There’s two things in the database. One, which is a timeline, which is named and groups a series of entries, and the other is the entries. Lines 6 through 10 defines the Timeline
and the Entry
object starts on line 12. Let me just scroll down here for you.
05:56
The Entry
has four fields. Three of them are defined here, title
, length
, and the timeline
that’s associated with it.
06:05
And the fourth is hidden in the abstract model called RankedModel
. The RankedModel
contains a field called rank
. This is an integer that is 1
or more and indicates the order of the entry.
06:19
Inside of the Meta
class, I specify the ordering as the "rank"
field to make sure that when these are queried, they come back in rank order.
06:32
Here’s the views
file that defines the two pages in my single-page application. Line 6 is the homepage that lists all of the timelines, and line 14 boots up the single-page application part of this single-page application.
06:48
The edit_timeline()
view takes an ID, which is the timeline that is being edited, and then it renders the edit_timeline.html
file.
06:56 That file, amongst other things, contains the information that Vue.js needs to render the page.
07:07
Up until now, I’ve been showing you the core app, which is for the timelines. I’ve also created another app called the api
. I’m grouping all of the things that’ll be available through REST inside of the Django api
app.
07:21
That includes the serializers.py
file. Inside of the serializers
file, I have two serializers—one for the timelines and one for the entries.
07:32
The TimelineSerializer
contains the entries within it, so I’m nesting Entry
objects inside of the timeline. Here’s another use of the source
attribute. The name entry_set
might be a little confusing inside of the HTML or for anyone who isn’t familiar with Django, so I’ve changed the serializer to call the entries entries
.
07:54
The EntrySerializer
then has the source
parameter of "entry_set"
, telling the DRF that the entries
field should be populated based on entry_set
of the Timeline
.
08:06
Because there are multiple entries, many
is set to True
. And so that I can create a Timeline
without specifying and serializing entries to go with it, I’ve set read_only
equal to True
as well.
08:18
The entry itself contains the "id"
and the "url"
of the Entry
, the "title"
, "length"
, and "rank"
of the Entry
. It also contains the "timeline"
field.
08:28
This will be the id
of the foreign key of the Timeline
object. This is necessary because you can’t have an entry without it pointing at a timeline, so when you go to create a new Entry
, you’re going to need that Timeline.id
.
08:47
Inside of the api/views
file, I define the ViewSets for the DRF views that I’m going to use in the application. The first two items here are the vanilla ViewSets for the Timeline
and the Entry
. I’ve specified their corresponding serializer classes and the corresponding querysets.
09:06
Let me just scroll down. The abstract RankedModel
class has logic inside of it that ensures that the ranks stay in order. The logic of all of this is done inside of an overridden .save()
method.
09:20
The DRF does not call the .save()
method on an object. It directly updates the database. Any logic in an overridden .save()
is not going to get executed. As a result, I’ve defined a new view here that is specific to changing the rank.
09:38
I’m not going to use the built-in update methods of the DRF—I’m going to update it myself manually to ensure that the Entry.save()
method gets called. Except for this being a special case as to why I’m doing it, the rest of it is a view like you’ve seen before.
09:56
I’m using the @api_view
decorator to indicate that this is a GET
. This is a little hack-ish. To be more technically correct, I probably should have made this a PUT
or a PATCH
, because I am editing an entry.
10:10
But the URL that I’m going to be using for change_rank()
will include the ID of the entry and its new rank. Because I’m doing this inside of the URL, I’m sticking with a GET
.
10:21
This is one of those many cases where REST is kind of a suggestion rather than a rule. Using the entry_id
, I get the object. I change the .rank
to the new_rank
provided and call the overridden .save()
.
10:36 I then re-query all of the entries that are associated with this timeline, and serialize them, sending them back down. This means when a move has been executed the Vue.js application can get all the data at once as to what the new ranks are of all the entries that were affected.
10:57
This is part of the edit_timeline.html
file. The body_script
block is where all of the JavaScript pieces get set up. The key to interfacing with the DRF is passing a URL that specifies the timeline REST API action point to the Vue code.
11:18
I’ve done this through the url
tag when this page gets rendered. The tag returns the timeline point for the REST view and sets that URL inside of the JavaScript global variable FETCH_URL
.
11:33
The Vue.js application will then use this to fetch the payload and populate the page. The remainder of this block is the loading of a variety of JavaScript files, including those to do the bsmodals
dialogs, a utilities function that deals with CSRF prevention, the actual Vue.js application inside of timelines.js
, and a third-party library that allows me to do drag-and-drop with Vue.js.
12:08
This is the timelines.js
file. This is where the master object for Vue.js is created. That’s on line 4, and it’s put in a variable called vm
.
12:20
You’ll see the use of that variable throughout this file. In lines 8 through 11, I create the data for the Vue.js object. .has_loaded
is used to indicate whether or not the screen should be rendered yet. timeline_id
, name
, and entries
are all the JSON that come from the server—timeline_id
and name
being from the Timeline
object, and entries
being the array of JSON that specify the entries on the display.
12:53
Django has a whole bunch of security mechanisms built-in, one of which is to prevent problems with form posts. This is done through the CSRF token. The ajax_setup()
and get_cookie()
methods come out of the utils.js
file and are used to make sure that a POST
can happen.
13:12 All Django posts must include a CSRF token, so this little bit of code gets that token in the right place.
13:20
Once everything’s loaded on the page, the last call here is to refresh_view()
. This method is what actually does the REST call. Inside of refresh_view()
, I use jQuery’s .getJSON()
method, passing in that global FETCH_URL
variable.
13:36
.getJSON()
takes a response()
function, and inside the body of that response()
function, I’m setting up the Vue.js data attributes.
13:47
.timeline_id
and .name
both come out of the response
field from the timeline object, and .entries
is copied from the array of entries
.
13:57
Once those three values have been put inside of the Vue.js object, I set .has_loaded
to true
to get the screen to render.
14:09
The Vue.js template that is rendered included the Add button. There’s a hook on the Add button that says when the button is pushed, call the method add_entry()
.
14:20
That’s defined here on line 14. Inside of the add_entry()
, I create a dictionary that contains the ID of the timeline object for the entry that’s being created, and a rank.
14:32 The rank is set to be one more than the last rank currently in the listing.
14:38
The bsmodals
library has a way of declaring forms and using REST methods to update objects that are associated with them. In line 22, I’m using the change_form()
—which you saw, that was popped up when you hit the Add button—and I’m calling the .show_create()
method on this form.
14:58
.show_create()
knows how to talk to REST using a URL, doing a POST
, creating a new object. The first parameter to this method is the URL of the POST
, '/api/v1/entries/'
, because I’m creating a new entry.
15:16 The second parameter is the default data for the dialog box that’s being popped up. The dialog has two hidden fields, which are the timeline and the rank, and then the two fields that you saw—the title and the length. That populates the dialog box.
15:33
When the user hits Save, the server’s contacted by the bsmodals
library, the REST POST
is done, the object is created, and then the third parameter of .show_create()
is a callback.
15:46
The response contains the newly created object. vm.entries.splice()
is a way of taking that response
object and gluing the result into the .entries
listing on the Vue.js object.
16:01 This adds the newly created entry to the list on the screen.
16:07
edit_entry()
works in a similar fashion. Once again, using the same form, this time calling a method called .show_patch()
. .show_patch()
takes the URL of the entry that was passed down as included as part of the JSON payload, the entry object itself, which it uses to populate the form, and then when the user hits Ok in the dialog box, a PATCH
is issued to the server. The REST code is triggered, the entry is updated. The third parameter to .show_patch()
is once again a callback. Once the entry has been patched, the server responds with the updated entry.
16:45
The Vue.js vm
object has the .entries
array spliced, replacing the old entry with the new one that was just edited.
16:57
You saw on the screen that I’m using a third-party library for Vue.js called Draggable. The Draggable library comes with its own tag, and inside of that tag, I have specified that when a drag action completes, that this method entry_moved()
gets called. You’ll recall that I don’t want to just call PATCH
here on the rank number, because I need the object’s .save()
method to get called on the server.
17:25
So I’m calling a specific, custom REST action instead. I do that by using jQuery’s .getJSON()
. The URL is the change_rank/
URL that I showed you earlier, complete with the ID of the entry that’s being changed and its new rank. And .getJSON()
has a callback,
17:46
and I take all of the entries that come back from the change_rank/
and replace the client-side copy.
17:54
Similar to the Add button, each Delete button is also wired to a function. That’s this delete_entry()
method. Inside of that, I’m using another dialog box out of the bsmodals
library called confirm
. confirm
shows a title and whatever text you give it and presents a Yes and No button.
18:14 Then it has a callback mechanism that checks whether or not the user hit Yes or No. On line 52, I look for a negative result and do nothing.
18:24
The REST call happens on line 56. If you get here, then the person hit Yes. It takes the Entry
object on the client side, grabs the URL, and does a straightforward AJAX call using HTTP DELETE
on that URL. The object has now been deleted on the server side.
18:46 There were a lot of moving pieces in that. I hope it wasn’t overwhelming. My intent was just to give you a flavor of how the REST pieces can be used inside of a single-page application.
18:57 If you’re truly interested in how all those pieces work, you’ll probably want to dig into the sample code and walk through it yourself. Next up, the last lesson—a summary of everything you’ve learned.
Christopher Trudeau RP Team on Sept. 3, 2021
Thanks Shivan, appreciated!
Artur Schmal on Jan. 6, 2023
Thank you for, another, great course Christopher. I’ve tried to rebuild the SPA application by typing along with the Python stuff. I ran into problems when using the default contents of timelines/apps.py
: it didn’t show the entries in the timelines. When I removed the line default_auto_field = 'django.db.models.BigAutoField'
it did load the entries.
Christopher Trudeau RP Team on Jan. 6, 2023
Hi Artur,
It has been a while since this course came out and there are likely subtle Django things that might get in the way. I haven’t played with the problem you describe, but I do know that BigAutoField
is a more recent addition to Django. I suspect the problem you ran into is version specific.
At some point, maybe I’ll do an update for Django 4.1.
Become a Member to join the conversation.
Shivan Sivakumarnan on Sept. 3, 2021
Beautifully taught here!