Let’s Make an RSS Reader

Kinman Covey

© Utan Digital LLC 2018


Quick History of RSS        3

Trivia        3

1: Initial Setup        3

Install virtualenv        3

Create virtualenv        3

Initialize the Virtual Environment        3

Install Django        3

Create Django Project        4

Install remaining dependencies        4

Create Django Site        4

Add our site as an application        4

Write our first view        4

Write our first URL        5

Include our app’s URL routes        5

Perform our first migration        5

Create our admin user        6

2: Getting familiar with our data        6

Start our shell        6

Pull an RSS feed        6

3: Creating the templates        7

Setting up the templates folder        7

Create our base template        7

4: Writing the first tests        8

Testing for no input        9

Testing for user input        10

Running the tests        10

5: Building the backend        10

Extend our view        10

Rendering the template        11

Passing data into the template        12

Using data within the template        12

Render the RSS feed        13

Adding a form        14

Responding to input        14

Passing the tests        15

6: Saving user data        15

Write the Model tests        15

Create the Model        16

Migrate the changes        17

Pass the Feed Model tests        17

7: Building a single page application        17

Create the RESTful tests        17

Create the serializer        20

Create the RESTful views        20

Add the new routes        23

Pass the tests        23

Create the SPA frontend        24

Create the Vue app        24

Create the SPA template        25

8: Styling the page        27

Include CSS libraries        27

Using Bootstrap styles        28

9: Frontend routing        29

Create a control page template        29

Add some basic routing        30

Responding to the route        30

10: Exercises        32

Easy        32

Medium        32

Hard        32

Legendary        32


Quick History of RSS

Trivia

1: Initial Setup

Install virtualenv

Create virtualenv

$ mkdir env && virtualenv env

Initialize the Virtual Environment

$ source env/bin/activate

Install Django

Create Django Project

$ django-admin startproject rssreader

Note: Server can be run at this point ($ python manage.py runserver)

Install remaining dependencies

Create Django Site

$ python manage.py startapp rss

Add our site as an application

Open rssreader/settings.py.

Modify the INSTALLED_APPS list to include rss.apps.RssConfig and rest_framework.

INSTALLED_APPS = [
   
'django.contrib.admin',
   
'django.contrib.auth',
   
'django.contrib.contenttypes',
   
'django.contrib.sessions',
   
'django.contrib.messages',
   
'django.contrib.staticfiles',
   
'rest_framework',
   
'rss.apps.RssConfig',
]

Write our first view

Open rss/views.py.

from django.http import HttpResponse

def index(request):
   
return HttpResponse("RSS Reader Index View")

Write our first URL

Create new file rss/urls.py.

from django.urls import path

from . import views

urlpatterns = [
   path(
'', views.index, name='index')
]

Include our app’s URL routes

Open rssreader/urls.py.

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
   path(
'admin/', admin.site.urls),
   path(
'rss/', include('rss.urls')),
]

Perform our first migration

A Django website is essentially many small applications, including ours, stitched together. Some of these applications require database functionality so we need to perform an initial migration to get those tables setup.

$ python manage.py migrate

Note: even if our application doesn’t use any models or database features, many of the important Django applications do. If you ever want to login to the admin panel, or allow sessions, you need to perform this initial migration.

Create our admin user

To manage our application later on, we’ll need an administrator account.

Note: this is the login used at our “/admin” route

$ python manage.py createsuperuser

2: Getting familiar with our data

We will be dealing primarily with RSS feeds in the form of XML. Let’s get familiar with this data by playing around in the python shell.

Start our shell

In our virtualenv, start a python shell and import the feedparser library.

$ python

Then

>>> import feedparser

Pull an RSS feed

>>> feed = feedparser.parse('https://www.djangoproject.com/rss/weblog/')

Now, feed will be a dictionary with all of our RSS data! We can access all of the articles and metadata about this website’s content straight from here.

This is more-or-less how we will be pulling the RSS feed in our view.

Note: to dig deeper into the feedparser library, check out its documentation here: https://pythonhosted.org/feedparser/introduction.html

3: Creating the templates

The frontend in Django is handled via HTML templates that allow for dynamic content. We can pass data to the templates from our views or just render the template as is.

Templates can also inherit from one another and utilize ‘blocks’ to allow for dynamic content between child templates.

Setting up the templates folder

I like to provide a base template for my Django projects to hold all the necessary boilerplate.

Create new directories rss/templates and rss/templates/rss.

Your folder structure should be similar to this:

The templates/rss folder is where all of our templates will go.

Note: you can specify custom template locations in your settings.py file, but the default is to look within a templates/[app name] directory

Create our base template

Create new file base.html within the templates/rss directory.

<!DOCTYPE html>
<
head>
   <
title>Django RSS Reader</title>
</
head>
<
body>
{% block body %}{% endblock %}
</
body>
</
html>

The block directive, body, is where our content from child templates will go.

Create new file reader.html within the templates/rss directory.

{% extends 'rss/base.html' %}

{% block body %}
<
p>Greetings from reader.html!</p>
{% endblock %}

Notice the extends directive at the very top. This tells Django that reader.html is a child of base.html. The child will inherit all of the markup from the parent and gain access to its blocks. Notice that we use the body block and add a paragraph inside.

This is how Django structures HTML templates. Before we can use it properly, we need to construct our backend.

4: Writing the first tests

We want to go ahead and think through our problem in terms of code. The best way to accomplish that is to write tests for our use cases.

We know we want our application to do a couple of things:

  1. Respond successfully
  2. Accept user input
  3. Return an RSS feed

We can go ahead and write a couple of tests based on these requirements. They’ll fail for now, but that’s okay. Our goal is to get them to pass by the end.

Open rss/tests.py.

Django does us a favor by providing some of the boilerplate for our tests. Let’s write a test class for our index view.

Modify rss/tests.py to match the following:

from django.test import TestCase

from django.urls import reverse

class RssIndexViewTests(TestCase):
   
def test_no_feed(self):
       
pass

   
def test_user_feed(self):
       
pass

We start off by importing the reverse helper function from the django.urls module. This allows us to refer to an endpoint by its name instead of its path. This comes in handy in case an endpoint ever changes.

With our empty test class in place, we can start creating our tests.

Testing for no input

Let’s write a test to describe how our application should behave when there is no RSS feed supplied.

Modify the test_no_feed method of the RssIndexViewTests class in rss/tests.py to match the following:

def test_no_feed(self):
   response = self.client.get(reverse(
"index"))

   self.assertEqual(response.status_code,
200)
   self.assertEqual(response.context[
"feed"], None)

This test uses the client object supplied by the TestCase class to “mock” an HTTP request to the index view.

After receiving the response, we use the assertEqual method of the TestCase class to test two things:

  1. Whether or not the view responded successfully (HTTP 200)
  2. Whether or not the feed object is None

Note: This applies a restriction on our view that will come into play down the line (feed being None when no feed is supplied).

This properly covers our bases for the index view having no user input.

Testing for user input

Now we need a test for the real functionality of our application.

Modify the test_user_feed method of the RssIndexViewTests class in rss/tests.py to match the following:

def test_user_feed(self):
   response = self.client.get(reverse(
"index") + "?url=https://www.djangoproject.com/rss/weblog/")

   self.assertEqual(response.status_code,
200)
   self.assertNotEqual(response.context[
"feed"], None)

This test is very similar to the test_no_feed method with the difference being the url query parameter appended to the endpoint. Since we are supplying a feed in this use case, we instead use the assertNotEqual method to ensure that the feed object is not None.

These tests correctly describe the goal of our application.

Running the tests

To run our application’s tests, we use the test utility provided by Django.

Enter the following line into your shell:

$ python manage.py test rss

You’ll notice that the tests fail. Don’t worry, they’re supposed to fail! Now we’re going to work on getting them to pass.

5: Building the backend

Now we get to the bread and butter of any Django application: views.

Extend our view

Open rss/views.py and import feedparser at the top.

import feedparser

Extend the index view to match the following:

def index(request):
   url =
'https://www.djangoproject.com/rss/weblog/'

   feed = feedparser.parse(url)

   
return HttpResponse(feed["feed"]["title"])

Start up the Django server if you haven’t yet.

$ python manage.py runserver

Open your browser and navigate to localhost:8000/rss.

Rendering the template

Now that we have a running view, let’s wire it up to our template.

Modify the return statement to match the following:

return render(request, 'rss/reader.html')

Using the render function, provided by the django.shortcuts module, we can easily specify that one of our templates should be returned as the response.

Refresh the web browser to see our template in action.

“I’m so proud of my children” - base.html

Passing data into the template

With the template in place, let’s pass our RSS feed into it.

Modify the index view return statement to match the following:

return render(request, 'rss/reader.html', {
   
'feed': feed
})

Passing that dictionary into the render function adds what Django calls context to the template. The context is a dictionary of data that the template can use for decision making, rendering, and control flow.

Using data within the template

Objects within the template’s context can be access directly by name.

Modify the reader.html template to match the following:

{% extends 'rss/base.html' %}

{% block body %}

<
h2>{{ feed.feed.title }}</h2>

{% endblock %}

Notice that we’re using the feed object passed in through the context map to render the RSS feed title.

Note: in a Django template, the double curly brace ‘{{ }}’ directives output the value of a variable rendering it straight into the HTML.

Refresh your browser to render the results.

Now we’re cooking with peanut oil

Render the RSS feed

Modify the reader.html file to include the following:

{% if feed.items %}

{% for item in feed.items %}
<
div>
   <
p>
       {{ item.published }}
       <
a href="{{ item.link }}">{{ item.title }}</a>
       {{ item.author }}
   </
p>
</
div>
{% endfor %}

{% endif %}

First thing, we’re testing for the proper existence of items within the feed object.

Then we use a for loop to iterate over each item and display several of its properties. We’re really interesting in seeing when the article was published, who wrote it, what its name is, and where to find it.

Refresh your browser to see this output.

Adding a form

We need to add a form to our template to allow for user input.

Modify reader.html and add the following at the top of the body block:

<form action="" method="get">
   <
input type="text" placeholder="RSS feed URL" name="url">
   <
button type="submit">Go</button>
</
form>

Responding to input

To get any use out of our form, we have to extend our view.

Modify the index view to match the following:

def index(request):
   
if request.GET.get("url"):
       url = request.GET[
"url"]
       feed = feedparser.parse(url)
   
else:
       feed =
None

   
return render(request, 'rss/reader.html', {
       
'feed': feed
   })

In just a few extra lines, we’ve gone from a static RSS reader to a completely dynamic one!

There is a problem, though, since our feed variable can be None. We should check for this in our template and display a message in case there isn’t a feed available.

Modify the rss/templates/reader.html file so that the markup under the form matches this:

{% if feed %}
<
h2>{{ feed.feed.title }}</h2>

{% if feed.items %}

{% for item in feed.items %}
<
div>
   <
p>
       {{ item.published }}
       <
a href="{{ item.link }}">{{ item.title }}</a>
       {{ item.author }}
   </
p>
</
div>
{% endfor %}

{% endif %}
{% else %}
<
p>Enter your favorite RSS feed above.</p>
{% endif %}

Refresh your browser and give it a try with your favorite RSS feed.

Passing the tests

Now we should be able to pass our tests. Go ahead and run this command in your shell:

$ python manage.py test rss

6: Saving user data

What if we want our app to remember the feeds we enter and load them automatically when we visit the page? Luckily, we can do this with Django Models.

Write the Model tests

As always, we start with tests.

Open rss/tests.py and add the following import.

from rss.models import Feed

Note: we are establishing a naming convention for our model, Feed, and setting a restriction for our app to name the model “Feed”

Modify rss/tests.py, adding the following test class:

class RssFeedModelTests(TestCase):

        
def setUp(self):
                Feed.objects.create(
                        url=
"https://www.djangoproject.com/rss/weblog/"
                )

        
def test_model_has_url(self):
                django_feed = Feed.objects.get(
                        url=
"https://www.djangoproject.com/rss/weblog/"
                )

                self.assertEqual(
                        django_feed.url,
                        
"https://www.djangoproject.com/rss/weblog/"
                )

Note: line breaks inserted for readability, not necessary in your code

This test sets some restrictions on our future Model:

  1. The Feed Model needs to have a URL property
  2. The URL property needs to store the URL that the feed is created with
  1. Note: It sounds obvious, I know, but the point of unit testing is to make no assumptions about the validity of our code other than to assume it needs to be tested

Create the Model

Open rss/models.py and add the following Model class:

class Feed(models.Model):
        url = models.URLField(max_length=
255, unique=True)

        
def __repr__(self):
                
return "<Feed '{}'>".format(self.url)

As you can see, we create the Feed model with one property: url. The URL is all we need to store an RSS feed for later consumption. We are also setting the unique property to True. This will ensure that any attempt at submitting a duplicate feed returns an error.

Migrate the changes

$ python manage.py makemigrations && python manage.py migrate

Pass the Feed Model tests

Go ahead and ensure that the tests are passing for our new addition.

$ python manage.py test rss

7: Building a single page application

Modern web applications are not driven by forms and page loads, they’re driven by asynchronous API calls and JavaScript. We can take our application and transform it into a modern SPA (single page application) with Vue.js and the Django Rest Framework.

Note: to get familiar with Vue.js beyond this lesson, check out its documentation here: https://vuejs.org/.

Note: to get familiar with the Django Rest Framework, check out its documentation here: http://www.django-rest-framework.org/.

Create the RESTful tests

Open rss/tests.py and add the following import statement:

import json

Add the following two test classes:

class RssRestFeedsViewTests(TestCase):

        
def test_create_feed(self):
                url =
"https://www.djangoproject.com/rss/weblog/"
                json_data = json.dumps({
"url": url })

                response = self.client.post(
                        reverse(
"rest-feeds"),
                        json_data,
                        content_type=
"application/json"
                )

                feeds = Feed.objects.all()

                self.assertEqual(response.status_code,
201)
                self.assertQuerysetEqual(
                        feeds,
                        [
"<Feed '{}'>".format(url)]
                )

        
def test_get_feeds(self):
                url =
"https://www.djangoproject.com/rss/weblog/"

                Feed.objects.create(
                        url=url
                )

                response = self.client.get(reverse(
'rest-feeds'))
                feed = response.json()[
0]

                self.assertEqual(response.status_code,
200)
                self.assertEqual(feed[
"url"], url)

        
def test_update_feed(self):
                url =
"https://www.djangoproject.com/rss/weblog/"
                new_url =
"https://utan.io/?feed=rss2"

                Feed.objects.create(
                        url=url
                )

                json_data = json.dumps({
                        
"url": new_url        
                })

                response = self.client.put(
                        
"/rss/feeds/1/",
                        json_data,
                        content_type=
"application/json"
                )

                feeds = Feed.objects.all()

                self.assertEqual(response.status_code,
200)
                self.assertQuerysetEqual(
                        feeds,
                        [
"<Feed '{}'>".format(new_url)]
                )

        
def test_delete_feed(self):
                Feed.objects.create(
                        url=
"https://www.djangoproject.com/rss/weblog/"
                )

                response = self.client.delete(
"/rss/feeds/1/")

                feeds = Feed.objects.all()

                self.assertEqual(response.status_code,
200)
                self.assertQuerysetEqual(
                        feeds,
                        []
                )

class RssRestItemsViewTests(TestCase):

        
def test_get_items(self):
                Feed.objects.create(
                        url=
"https://www.djangoproject.com/rss/weblog/"
                )

                response = self.client.get(reverse(
"rest-items"))

                self.assertEqual(response.status_code,
200)

Once again, we are defining how our app will behave through our tests. Through this test, we can see that our Feed REST endpoint should accurately return the existing Models from the database, create new ones, update attributes, and delete Feeds without any issues.

We also test for a second endpoint that retrieves RSS items. Items aren’t one of our models, nor are they defined elsewhere in our app. This is because we won’t be storing them and will instead be retrieving them ondemand based on our stored Feeds.

Create the serializer

Create new file rss/serializers.py and add the following:

from rest_framework import serializers
from .models import Feed

class FeedSerializer(serializers.ModelSerializer):
        
class Meta:
                model = Feed
                fields = (
'id', 'url')

This serializer allows the Django Rest Framework to properly transform our Feed Model to JSON, and vice versa. Using the ModelSerializer allows us to skip most of the boilerplate, such as explicitly defining each field that our Model has within the serializer class.

Create the RESTful views

Open rss/views.py and add the following import statements:

from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
from .serializers import FeedSerializer
from .models import Feed

Within the rss/views.py file, add the following view:

@csrf_exempt
def rest_feeds(request):
        
if request.method == "GET":
                feeds = Feed.objects.all()
                serializer = FeedSerializer(feeds, many=
True)
                
return JsonResponse(serializer.data, safe=False)

        
elif request.method == "POST":
                data = JSONParser().parse(request)
                serializer = FeedSerializer(data=data)

                
if serializer.is_valid():
                        serializer.save()
                        
return JsonResponse(serializer.data, status=201)

                
return JsonResponse(serializer.errors, status=400)

Notice the csrf_exempt decorator at the top of the definition. This allows for requests to this endpoint to be POST requests and not need a CSRF token.

Note: this decorator should not be used liberally and should be used with the utmost caution in production. Since this app is solely a local web app for personal use, it’s safe.

Getting every Feed at once is great, but there will be cases down the road when we need to fetch just one for viewing or modification.

Add the following view to rss/views.py:

@csrf_exempt
def rest_feeds_detail(request, pk):
        
try:
                feed = Feed.objects.get(pk=pk)
        
except Feed.DoesNotExist:
                
return HttpResponse(status=404)

        
if request.method == "GET":
                serializer = FeedSerializer(feed)
                
return JsonResponse(serializer.data)

        
elif request.method == "PUT":
                data = JSONParser().parse(request)
                serializer = FeedSerializer(feed, data=data)

                
if serializer.is_valid():
                        serializer.save()
                        
return JsonResponse(serializer.data)

                
return JsonResponse(serializer.errors, status=400)

        
elif request.method == "DELETE":
                feed.delete()
                
return HttpResponse()

Finally, let’s add our items endpoint.

Add the following view to rss/views.py:

@csrf_exempt
def rest_items(request):
        feeds = Feed.objects.all()

        items = []

        
for feed in feeds:
                rss = feedparser.parse(feed.url)

                
try:
                        items.extend(rss[
"items"])
                
except KeyError:
                        
continue

        items = list(reversed(sorted(items, key=
lambda item: item["published_parsed"])))

        
return JsonResponse(items, safe=False)

The functionality of this view should look familiar as we’re using the feedparser module to grab all of the items from each of our stored Feed Models. Once we have each item, we sort them by their published_parse attribute using the built-in sorted and reversed methods. After that, we return it as a JsonResponse.

Note: setting the safe flag to False allows us to send objects other than Python dictionaries, e.g. a list of dictionaries

Add the new routes

Open rss/urls.py and add the following import statement:

from django.urls import re_path

The add the following endpoints to urlpatterns:

path('feeds/', views.rest_feeds, name='rest-feeds'),
re_path(
r'^feeds/(?P<pk>[0-9]+)/$', views.rest_feeds_detail, name='rest-feeds-detail'),
path(
'items/', views.rest_items, name='rest-items')

This should do it for our RESTful endpoints. Now we just need to update our index view to simply handle rendering our template.

Open rss/views.py and modify the index view to match the following:

def index(request):
        
return render(request, 'rss/reader.html')

This would also be a good time to remove the RssRestFeedsViewTests class altogether since this view no longer services any complex functionality.

Pass the tests

Let’s ensure that our tests are passing now that our endpoints are created:

$ python manage.py test rss

“I do not pilfer victory.” - Alexander the Great, allegedly

Create the SPA frontend

Let’s go ahead and install Vue.js within our template.

Open base.html and include this tag at the bottom of the head:

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

Note: this is the development build of Vue. It provides some more descriptive console errors and other nice developer features so it’s perfect for this exercise.

Create the Vue app

Create a new folder in the rss directory called static.

In the rss/static folder, create a new folder called rss.

Note: this should be a familiar structure (think templates, from earlier)

Create new file rss/static/rss/app.js and add the following:

var rssApp = new Vue({
        
el: '#rss-app',
        
        
data: {
                
items: [],
                
feeds: [],
                
newLink: ""
        },

        
methods: {
                
api: function(endpoint, method, data) {
                        
var config = {
                                
method: method || 'GET',
                                
body: data !== undefined ? JSON.stringify(data) : null,
                                
headers: {
                                        
'content-type': 'application/json'
                                }
                        };

                        
return fetch(endpoint, config)
                                        .then((response) => response.json())
                                        .catch((error) =>
console.log(error));
                },

                
reload: function() {
                        
this.getItems();
                        
this.getFeeds();
                },

                
getFeeds: function() {
                        
this.api("/rss/feeds/").then((feeds) => {
                                
this.feeds = feeds;
                        });
                },

                
getItems: function() {
                        
this.api("/rss/items/").then((items) => {
                                
this.items = items;
                        });
                },

                
newFeed: function() {
                        
this.api("/rss/feeds/", "POST", { url: this.newLink }).then(() => {
                                
this.reload();
                        });
                },

                
deleteFeed: function(id) {
                        
this.api("/rss/feeds/" + id + "/", "DELETE").then(() => {
                                
this.reload();
                        });
                }
        }
});

Create the SPA template

Create new file rss/templates/rss/feeds.html and add the following:

<div>
        {% verbatim %}
        <
div v-if="items.length" v-for="item in items">
                <
p>
                        {{ item.published }}
                        <
a v-bind:href="item.link">{{ item.title }}</a>
                        {{ item.author }}
                </
p>
        </
div>
        {% endverbatim %}
</
div>

Notice the verbatim tag used after the opening element. This Django tag allows us to signify that the content within this tag should be sent to the client as-is with no modification by the renderer. This is crucial for applications that use frontend frameworks such as Vue, Handlebars, or any other that has conflicting syntax with Django’s templating engine.

While we’re at it, let’s go ahead and modify our reader.html file to use our new Vue app and its template.

Modify the body block to match the following:

{% block body %}
<
div id="rss-app">
        <
div>
                <
input type="text" v-model="newLink" placeholder="RSS feed URL">
                <
button @click="newFeed">Save</button>
        </
div>

        {% include 'rss/feeds.html' %}
</
div>

{% load static %}
<
script type="application/javascript" src="{% static 'rss/app.js' %}"></script>
<
script type="application/javascript">
window.addEventListener("load", function() {
        rssApp.reload();
});
</
script>
{% endblock %}

Notice that we’re giving this main div tag an id of “rss-app”. This matches the el property of our Vue app. This allows the Vue app to take over the element and allow for dynamic rendering.

We are also using the include tag to add our new template to the markup of readers.html.

Towards the bottom, we use the load static directive to allow for retrieving our static JavaScript file using relative means (as opposed to using an absolute link that may change).

Finally, we add some inline JavaScript to kickstart our rssApp on page load.

Restarting the server and going to https://localhost:8000/rss/ shows our app in a fresh state:

Entering an RSS feed URL and clicking “Save” will result in the following:

As you can see, our tests were correct! We were able to save an RSS feed and pull its items without any hassle.

8: Styling the page

Include CSS libraries

Let’s include Bootstrap to assist us in quickly making this look nicer.

Modify the base.html file and add this line within the head tag:

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css">

Using Bootstrap styles

Modify the reader.html file and add classes where you see fit. Play around with the styling and find something that you enjoy. For reference, here is mine:

Note: if you are unfamiliar with Bootstrap, feel free to copy my markup below or familiarize yourself at https://getbootstrap.com/docs/4.0/getting-started/introduction/

<div id="rss-app" class="container mt-4">
        <
div class="row mb-4">
                <
div class="col-4">
                        <
input type="text" v-model="newLink" placeholder="RSS feed URL" class="form-control">
                </
div>
                <
div class="col-2">
                        <
button class="btn btn-secondary" @click="newFeed">Save</button>
                </
div>
        </
div>

        {% include 'rss/feeds.html' %}
</
div>

And here is my feeds.html template:

<div>
        {% verbatim %}
        <
div v-if="items.length" v-for="item in items" class="row mb-2">
                <
div class="col">
                        {{ item.published }}
                        <
a v-bind:href="item.link">{{ item.title }}</a>
                        {{ item.author }}
                </
div>
        </
div>
        <
div v-if="!items.length">
                <
p>Enter your favorite RSS feed above.</p>
        </
div>
        {% endverbatim %}
</
div>

Refreshing your browser will show you the style changes.

9: Frontend routing

So far our app can save RSS feeds, retrieve links for our consumption, and does it all without so much as a page reload. However, we can’t administrate our content without going into the built-in Django admin panel. It would be nice if we could delete which feeds we’re subscribed to from within our application.

Luckily, we’ve already laid the groundwork for this exact functionality.

Create a control page template

Create new file rss/templates/rss/control.html and add the following:

<div>
        {% verbatim %}
        <
div class="row mb-2" v-for="feed in feeds">
                <
div class="col">
                        <
a v-bind:href="feed.url">{{ feed.url }}</a>
                </
div>
                <
div class="col text-right">
                        <
a @click="deleteFeed(feed.id)" class="text-danger">Delete</a>
                </
div>
        </
div>
        {% endverbatim %}
</
div>

Add some basic routing

Open app.js and add the following entry to the data map:

route: "feeds"

In the same file, add the following method to the methods map:

setRoute: function(route) {
        
this.route = route;
},

It would also be nice if our app could alter the URL and load the appropriate section if a page was to be reloaded. Add a new method, setup, to the methods map:

setup: function() {
        
var hash = window.location.hash;

        
if(hash) {
                
this.route = hash.slice(1);
        }

        
this.reload();
},

Now change the inline script within reader.html to match this new pattern:

<script type="application/javascript">
window.addEventListener("load", function() {
        rssApp.setup();
});
</
script>

Responding to the route

Open reader.html and modify its content to match the following:


<div id="rss-app" class="container mt-4">
        <
ul class="nav mb-4">
                <
li class="nav-item">
                        <
a class="nav-link" href="#feeds" @click="setRoute('feeds')">Feeds</a>
                </
li>
                <
li class="nav-item">
                        <
a class="nav-link" href="#controls" @click="setRoute('controls')">Controls</a>
                </
li>
        </
ul>

        <
div class="row mb-4">
                <
div class="col-4">
                        <
input type="text" v-model="newLink" placeholder="RSS feed URL" class="form-control">
                </
div>
                <
div class="col-2">
                        <
button class="btn btn-secondary" @click="newFeed">Save</button>
                </
div>
        </
div>

        <
div v-if="route == 'controls'">
                {% include 'rss/control.html' %}
        </
div>
        <
div v-if="route == 'feeds'">
                {% include 'rss/feeds.html' %}
        </
div>
</
div>

Restarting the web server and reloading the page will show our SPA in action!


10: Exercises

Here are a few exercises that should go a long way in terms of making it your own:

Easy

  1. Use the bootstrap library to modernize the look and feel
  1. Example: Use the card classes for each article
  2. Example: Use the typography and color classes to switch things up
  1. Display the summary attribute of each item to allow for quick sampling of the article

Medium

  1. Use the published_parsed attribute of each item to display a nicer, cleaner date within the template
  2. Add a search feature that allows the user to search the retrieved items for keywords

Hard

  1. Extend the controls and the rssApp object to allow for modifying existing Feeds
  1. Example: Allow the user to update the URL of a Feed they’ve saved
  1. Add properties to the Feed Model allowing for advanced sorting, storing, and display
  1. Example: Allow the user to give each feed a tag that they can sort by
  1. Create a new Model, Omission, with matching views and functionality within the Vue app that allows a user to “omit” an article from displaying
  1. Explanation: Each item has a button that you can click if you don’t want to see a certain article ever again. Once clicked, the item is stored as an Omission which the views check against when fetching items from your feeds.

Legendary

  1. Build a recommendations engine that allows you to upvote/downvote items, stores them as their own dedicated Model, and uses them for scoring current items within a “recommendations” endpoint/page of the app.
  1. Note: if you complete this one, please send a GitHub link to kinman@utan.io. I will honor you accordingly.