Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Locked learning resources

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

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.

00:12 Even for a relatively small application, there’s a fair number of moving parts here. There’s HTML, there’s Python, there’s JavaScript, there’s Vue.js-specific JavaScript, and Vue.js-specific templates.

00:27 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.

Avatar image for Shivan Sivakumarnan

Shivan Sivakumarnan on Sept. 3, 2021

Beautifully taught here!

Avatar image for Christopher Trudeau

Christopher Trudeau RP Team on Sept. 3, 2021

Thanks Shivan, appreciated!

Avatar image for Artur Schmal

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.

Avatar image for Christopher Trudeau

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.