Django & React Project: Look at some jokes and smile :)

By Samuel Muiruri | Dec. 20, 2018 | Django


It’s the #100DaysOfCode and as one of my projects I decided to make a streaming app in React. I found a dataset of jokes saved into a csv that gave me good content to populate the site with. Live demo of the site here and the source code available.

I separated them into three classes: Joke which includes R rated jokes, DadJokes PG rated jokes “mainly” and Tweet short and to the point jokes. All the models have one TextField for the joke with a unique rule to avoid duplicates.

from django.db import models
class DadJokes(models.Model):
 text = models.TextField(unique=True)
def __str__(self):
  return self.text
class Tweet(models.Model):
 text = models.TextField(unique=True)
def __str__(self):
  return self.text
class Joke(models.Model):
 text = models.TextField(unique=True)
def __str__(self):
  return self.text

The page that serves the html only gets the title from django dynamically, the rest of the content I’ll load with an ajax request from the server.

def home(request, template_name="home.html"):
 context = {'title': 'Jokes Central'}
 return render_to_response(template_name, context)

The home template home.html has the react script and one div with an id root , it’s going to be the one populated by react. React also to work here need the type on the script declared this way type="text/jsx"

{% extends 'base.html' %}
{% block js %}
  <script type="text/jsx" src="/static/js/react-script.js"></script>
{% endblock %}
{% block content %}
  <div id="root" class="top-padding">
  </div>
{% endblock %}

The base template it inherits from has react and babel to make react run “natively” on the browser.

<!DOCTYPE html>
<html>
<head>
 <title>{% block title %}{{ title }}{% endblock %}</title>
<meta charset="utf-8"/>
<link rel="stylesheet" type="text/css" href="/static/css/bootstrap.min.css">
 <link rel="stylesheet" type="text/css" href="/static/css/style.css">
 <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" integrity="sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp" crossorigin="anonymous">
{% block css %} {% endblock %}
<script src="/static/js/react.development.js"></script>
    <script src="/static/js/react-dom.development.js"></script>
    <script src="/static/js/babel.min.js"></script>
{% block js %}{% endblock %}
</head>
<body>
 <!-- Image and text -->
 <nav class="navbar navbar-light">
   <a class="navbar-brand" href="#">
     <img src="/static/img/bootstrap-solid.svg" width="30" height="30" class="d-inline-block align-top" alt="">
     Bootstrap
   </a>
 </nav>
{% block content %}{% endblock %}
</body>
</html>

For me, I opted to use the development version of react instead of production which for now seems to recommend you use npm and NodeJS as the running place for ReactJS. Downside of this is that you also use it to serve the templates leaving Django as an obscure backend to both Node and ReactJS.

react-script.js code is as follows: It has one Body Component with a constructor with the default starting content. An empty data list, classes for the radio button like first_active the ref_id of where to start querying from joke_id each of the three with it’s own id and first_checked which sets/checks to see which of the three radio buttons value is true/checked.

The refresh function is what’s called to get new jokes. Based on the values of the constructor, which change as you interact with the radio buttons of the type of jokes

Screenshot of the page, the radio buttons on the right keep the id of last joke received and whether it’s selected

If you select another button like Tweet or DadJokes then press refresh icon (it has an onclick handler that runs the refresh function

<div className="col-md-12 top-padding" align="center">
                  <i className="fas fa-sync-alt" onClick={() => this.refresh()}></i>
                </div>

You can find the above snippet in the render function that renders html to a div element. Refresh then with the data it needs to send with the ajax post passes it to postdata and it gets the relevant jokes for the selected type and returns it.

componentDidMount runs when React has finished loading. It runs postdataand also a setInterval that runs refresh after every 30 seconds to get new jokes.

update_checked works by toggling the classes on the bootstrap radio so that on click it becomes active/checked and the relevant css classes are changes so it’s also added the class active while removing it for the other buttons. This is linked to the button with an onChange click handler that links to this.

postdata when it gets new jokes will first get the current jokes so it can append the new one at the start and then setState will update the page with the new list as well as change the ref_id so on the next call will pick it up from there.

Finally render is where the div’s are created and populated, the jokes using a map function to iterate over all the items and ReactDOM.render binds the results from Body to the div root and basically populates it’s entire content with it.

You should note btw that it will replace any content inside the root div if you have any.

class Body extends React.Component {
    constructor(props) {
       super(props);
       this.state = { 
           data : [],
first_active: 'active',
           second_active: '',
           third_active: '',
joke_id: 1,
           dadjoke_id: 1,
           tweet_id: 1,
first_checked: true,
           second_checked: false,
           third_checked: false
        }
    }
refresh() {
      var model_name, id;
      if (this.state.first_active == 'active') {
        model_name = 'Joke';
        id = this.state.joke_id;
      } else if (this.state.second_active == 'active') {
        model_name = 'DadJokes';
        id = this.state.dadjoke_id;
      } else if (this.state.third_active == 'active') {
        model_name = 'Tweet';
        id = this.state.tweet_id;
      }
var initial_data = {'model-name': model_name, 'id': id};
      this.postdata(initial_data)
    }
componentDidMount(){
     this.postdata()
     var self = this;
     
     setInterval(function(){ //get new content after 10 seconds 
        self.refresh() 
      }, 30000);
    }
update_checked(option) {
        if (option === 1) {
          this.setState({ first_active: 'active', first_checked: true, second_active: '', second_checked: false, third_active: '', third_checked: false })
        } else if (option === 2) {
          this.setState({ first_active: '', first_checked: false, second_active: 'active', second_checked: true, third_active: '', third_checked: false })
        } else if (option === 3) {
          this.setState({ first_active: '', first_checked: false, second_active: '', second_checked: false, third_active: 'active', third_checked: true })
        }
    }
postdata(initial_data = {'id': 1, 'model-name': 'Joke'}){
        var self = this;
        fetch("\start-jokes\/", {
         body: JSON.stringify(initial_data),
         cache: 'no-cache', 
         credentials: 'same-origin', 
         headers: {
          'user-agent': 'Mozilla/4.0 MDN Example',
          'content-type': 'application/json'
         },
         method: 'POST',
         mode: 'cors', 
         redirect: 'follow',
         referrer: 'no-referrer',
         })
         .then(response => response.json()).then((json) => {
            var current_data = this.state.data;
            var new_data = json['jokes'].concat(current_data);
            if (initial_data['model-name'] == 'Joke') {
              self.setState({ data : new_data, joke_id: json['ref-id'] }) 
            } else if (initial_data['model-name'] == 'DadJokes') {
              self.setState({ data : new_data, dadjoke_id: json['ref-id'] })
            } else if (initial_data['model-name'] == 'Tweet') {
              self.setState({ data : new_data, tweet_id: json['ref-id'] })
            }
           
        })
    }
render(){
       return( 
            <div>
              <div id="side-nav">
                <div className="btn-group btn-group-toggle" data-toggle="buttons">
                  <label className={`btn btn-secondary ${this.state.first_active}`}>
                    <input type="radio" name="options" id="option1" autoComplete="off" value="Joke" ref_id={this.state.joke_id} defaultChecked={this.state.first_checked} onChange={() => this.update_checked(1)} /> Joke
                  </label>
                  <label className={`btn btn-secondary ${this.state.second_active}`}>
                    <input type="radio" name="options" id="option2" autoComplete="off" value="DadJokes" ref_id={this.state.dadjoke_id} checked={this.state.second_checked}  onChange={() => this.update_checked(2)} /> DadJokes
                  </label>
                  <label className={`btn btn-secondary ${this.state.third_active}`}>
                    <input type="radio" name="options" id="option3" autoComplete="off" value="Tweet" ref_id={this.state.tweet_id} checked={this.state.third_checked} onChange={() => this.update_checked(3)} /> Tweet
                  </label>
                </div>
                <div className="col-md-12 top-padding" align="center">
                  <i className="fas fa-sync-alt" onClick={() => this.refresh()}></i>
                </div>
              </div>
             
             {this.state.data.length == 0 && 
                <div> No options available.</div>
             }
             {this.state.data.length > 0 && 
               <div className="container top-padding" id="jokes">
                    {this.state.data.map(function(item,i){
                       return(
                             <div key={item['key']} className="card col-md-7">
                                 <div className="card-body">
                                     {item['text']} 
                                 <p><span className="badge badge-secondary">{item.name}</span></p>
                                 </div>
                             </div>
                       )
                    })}
                </div>
             }
           </div>
         )
    }
}
// ========================================
ReactDOM.render(
  <Body />,
  document.getElementById('root')
);

The url postdata connects to the view joke_list , with ajax and for now since I’ve not worked on sending the csrf_token through the fetch request I’ve used csrf_exempt also on the view.

It get’s the model_name and ref_id, get’s back the actual model by using the load_model which returns back the model. The model and ref_id are passed to get_jokes that queries the model and returns 10 new jokes (or less if you’ve exhausted all the jokes, but they’re quite a lot).

The response from get_jokes , a new ref_id and jokes is added to the response dict and sent back.

@csrf_exempt
def joke_list(request):
    response = {'status': None}
if request.method == 'POST':
        data = json.loads(request.body)
        ref_id, model = data['id'], load_model(data['model-name'])
        response['jokes'], response['ref-id'] = get_jokes(model, ref_id, 10, data['model-name'])
        response['status'] = 'ok'
else:
        response['error'] = 'no post data found'
return HttpResponse(
            json.dumps(response),
            content_type="application/json"
        )

Load model has a dict with a key as the model_name and the model itself. First it checks to see if the provided name is in the model keys, if not returns the joke model else it uses the dict to query the model and return it.

get_jokes with the model, ref_id, maximum number of jokes to return and the model_name. It gets from the model the last_id of the model so the query doesn’t look beyond that.

inside a while loop it starting with the ref_id increments by 1 and checks to see if there’s an instance of this in the model. For every instance found the count is incremented by 1 so it will keep running until it reaches the maximum needed or ref_id is > last_id of the last added joke.

def load_model(model_name):
    models = {
        'Joke': Joke,
        'DadJokes': DadJokes,
        'Tweet': Tweet
    }
if model_name not in models.keys():
        return Joke
    else:
        return models[model_name]
def get_jokes(model, ref_id, maximum, model_name):
    count = 0
    list_jokes = []
    last_id = model.objects.latest('id').id
    while count < maximum and ref_id <= last_id:
        try:
            joke = model.objects.get(id=ref_id)
            joke_dict = {'key': '{0}-{1}'.format(model_name, joke.id), 'text': joke.text, 'name': model_name}
            list_jokes.append(joke_dict)
            count += 1
        except ObjectDoesNotExist:
            pass
ref_id += 1
return list_jokes, ref_id

And that’s it. In future I want to use it with websockets instead of ajax, if you’re curious and a good python dev maybe you can look at this. I’ve been trying to get it to work with the so far best option django-channels but I reached a snag.