Dynamically Created Sentences in Python/Django (madlibs)
Tags: python, django
Published: 2019-07-05 20:22:57

<p>This is a completely different type of blog post from all of the others that we've had before. Instead of focusing on recent media releases, this post will be very nerdy and go into how part of the new <a href="https://www.iceorfire.com/weekly-top-picks/">Weekly Top Picks</a> page works. <em>Caution: Python/Django code ahead!</em></p>

<p>The Weekly Top Picks page lists the top 3 releases of the week in each category based on their ratings. Each media section (books, theater movies, blu rays, albums) features a dynamically generated description paragraph. I'll describe how that works below.</p>

<p>We're dealing with three types of Django models: Book, Movie, and Album. The models are a bit more complicated than shown here but I'm trying to keep it simple.</p>

<h2>Movie class</h2>
<pre>
class Movie(models.Model):
    """
    Movie class
    """

    # Fields
    title = models.CharField(max_length=100, help_text="Enter title")
    release_date = models.DateField()
    bluray_date = models.DateField(null=True, blank=True)
    poster = models.CharField(max_length=200, help_text="Enter poster file")
    overview = models.TextField(help_text="Enter overview")
    genre = models.ManyToManyField(MovieGenre, help_text="Select a genre for this movie")
    mpaa_rating = models.CharField(max_length=10, help_text="MPAA Rating") #details call
    run_time = models.IntegerField()
    slug = models.SlugField(max_length=100, null=True, blank=True)
    rating = models.IntegerField(default=60)
    trailer = models.CharField(max_length=100, help_text="Enter trailer link")
    director = models.CharField(max_length=100, blank=True, help_text="Enter director")
    theater_link = models.CharField(max_length=255, null=True, blank=True, help_text="Enter theater link")
    bluray_link = models.CharField(max_length=255, null=True, blank=True, help_text="Enter blu-ray link")
    votes = models.IntegerField(default=0)
    num_ratings = models.IntegerField(default=0)
    running_total = models.IntegerField(default=0)
    weighted_rating = models.IntegerField(default=0)
</pre>

<h2>Album class</h2>
<pre>
class Album(models.Model):
    """
    Album class
    """

    # Fields
    artist = models.CharField(max_length=100, help_text="Enter artist")
    title = models.CharField(max_length=100, help_text="Enter title")
    image = models.CharField(max_length=100, help_text="Enter image file")
    big_image = models.CharField(max_length=100, help_text="Enter image file")
    release_date = models.DateField()
    record_type = models.CharField(max_length=25, help_text="Enter record type")
    slug = models.SlugField(max_length=100, null=True, blank=True)
    length = models.IntegerField() #in seconds
    link = models.CharField(max_length=255, blank=True, help_text="Enter purchase link")
    genre = models.ManyToManyField(AlbumGenre, blank=True, help_text="Select a genre for this album")
    base_rating = models.IntegerField(default=60)
    votes = models.IntegerField(default=0)
    num_ratings = models.IntegerField(default=0)
    running_total = models.IntegerField(default=0)
    weighted_rating = models.IntegerField(default=0)
</pre>

<h2>Book class</h2>
<pre>
class Book(models.Model):
    """
    Book class
    """

    # Fields
    title = models.CharField(max_length=150, help_text="Enter title")
    cover_image = models.CharField(max_length=200, help_text="Enter image file")
    description = models.TextField(help_text="Enter description")
    author = models.CharField(max_length=100, help_text="Enter author")
    pages = models.IntegerField()
    release_date = models.DateField()
    genre = models.ManyToManyField(BookGenre, help_text="Select a genre for this book")
    slug = models.SlugField(max_length=100, null=True, blank=True)
    rating = models.IntegerField(default=60)
    votes = models.IntegerField(default=0)
    link = models.CharField(max_length=255, blank=True, help_text="Enter purchase link")
    num_ratings = models.IntegerField(default=0)
    running_total = models.IntegerField(default=0)
    weighted_rating = models.IntegerField(default=0)
</pre>

<p>Next, we need a utility method to get a date range for this week. We get today's date and then figure out the day of the week with Sunday being 0 and Saturday being 6. We use that info to figure out the week's Sunday and Saturday which correspond to our start and end dates. Pass back the start and end dates!</p>

<pre>
def getThisWeekStartEnd():
    """
    return Sunday's and Saturday's datetime object for the current week
    """

    today = datetime.today().strftime("%Y-%m-%d")

    d1 = datetime.strptime(today, "%Y-%m-%d")
    day_of_week = d1.weekday()

    if day_of_week == 0:
        start_date = d1 + timedelta(days=-1)
        end_date = d1 + timedelta(days=5)
    elif day_of_week == 1:
        start_date = d1 + timedelta(days=-2)
        end_date = d1 + timedelta(days=4)
    elif day_of_week == 2:
        start_date = d1 + timedelta(days=-3)
        end_date = d1 + timedelta(days=3)
    elif day_of_week == 3:
        start_date = d1 + timedelta(days=-4)
        end_date = d1 + timedelta(days=2)
    elif day_of_week == 4:
        start_date = d1 + timedelta(days=-5)
        end_date = d1 + timedelta(days=1)
    elif day_of_week == 5:
        start_date = d1 + timedelta(days=-6)
        end_date = d1
    elif day_of_week == 6:
        start_date = d1
        end_date = d1 + timedelta(days=6)

    return start_date, end_date
</pre>

<p>Now that we have our start and end dates, let's write our views.py function to query for our data. We start by calling our function that we just wrote to get our start and end dates. Next, we query for 3 Book data objects that fall in our date range and are ordered in descending order (top results). Do the same thing for Movie and Album objects. Note: theater releases and blu rays are both Movies. In case we have no data for the current week, query for last week's data. (We should add in a fallback in case that doesn't return any data but this works for our purposes.) We have our data, so build our sentences... which we'll do below.</p>

<pre>
def top_picks(request):
    start_date, end_date = getThisWeekStartEnd()

    books = Book.objects.filter(release_date__range=[start_date, end_date]).filter(active=True).order_by('-rating')[:3]
    theaters = Movie.objects.filter(release_date__range=[start_date, end_date]).filter(active=True).filter(bluray_date__isnull=True).order_by('-rating')[:3]
    blurays = Movie.objects.filter(bluray_date__range=[start_date, end_date]).filter(active=True).filter(bluray_date__isnull=False).order_by('-rating')[:3]
    albums = Album.objects.filter(release_date__range=[start_date, end_date]).filter(active=True).order_by('-base_rating')[:3]

    if len(books) == 0:
        books = Book.objects.filter(release_date__range=[start_date + timedelta(days=-6), end_date + timedelta(days=-6)]).filter(active=True).order_by('-rating')[:3]

    if len(theaters) == 0:
        theaters = Movie.objects.filter(release_date__range=[start_date + timedelta(days=-6), end_date + timedelta(days=-6)]).filter(active=True).filter(bluray_date__isnull=True).order_by('-rating')[:3]

    if len(blurays) == 0:
        blurays = Movie.objects.filter(bluray_date__range=[start_date + timedelta(days=-6), end_date + timedelta(days=-6)]).filter(active=True).filter(bluray_date__isnull=False).order_by('-rating')[:3]

    if len(albums) == 0:
        albums = Album.objects.filter(release_date__range=[start_date + timedelta(days=-6), end_date + timedelta(days=-6)]).filter(active=True).order_by('-base_rating')[:3]

    books_text = build_book_sentences(books)
    theaters_text = build_movie_sentences(theaters)
    blurays_text = build_movie_sentences(blurays)
    albums_text = build_album_sentences(albums)

    return render(
        request,
        'top_picks.html',
        context={
            'books':books,
            'books_text':books_text,
            'theaters':theaters,
            'theaters_text':theaters_text,
            'blurays':blurays,
            'blurays_text':blurays_text,
            'albums':albums,
            'albums_text':albums_text
        },
    )
</pre>

<p>Great! We've got our data objects so let's build our dynamically generated sentences. We'll create three functions that are similar: one for Books, one for Movies, and one for Albums. All three work the same way... create a list of adjectives, a list of actions, and dynamically assign them as we loop over our media objects. Next we apply our variables to a randomly selected sentence structure. Finally, concat the sentences together and pass them back.</p>

<pre>
def build_book_sentences(books):
    sentences = ''
    adjectives = ['exciting', 'finely crafted', 'inventive', 'fresh', 'vivid', 'mesmerizing', 'imaginative']
    actions = [
        "invents a spellbinding world that draws you in. ",
        "grabs you and doesn't let go until you've read the last page. ",
        "paints a literary picture that makes you feel as though you're right there in the action. ",
        "presents a new take on a familiar genre with a page-turning style. ",
        "demands you curl up in a comfy chair and keep reading until the very last word. "
    ]

    used_adj = []
    used_action = []

    for book in books:
        sel_adj = random.choice(adjectives)
        sel_action = random.choice(actions)

        while sel_adj in used_adj or sel_action in used_action:
            sel_adj = random.choice(adjectives)
            sel_action = random.choice(actions)

        used_adj.append(sel_adj)
        used_action.append(sel_action)

        genre = book.genre.first().name.lower().replace('&', 'and')

        structure = [
            "{}'s {} new {} book, {}, {}".format(book.author, sel_adj, genre, book.title, sel_action),
            "In the {} {} book, {}, {} {}".format(sel_adj, genre, book.title, book.author, sel_action)
        ]

        sel_struct = random.choice(structure)
        sentences = sentences + sel_struct

    return sentences
</pre>

<pre>
def build_movie_sentences(movies):
    sentences = ''
    adjectives = ['exciting', 'finely crafted', 'inventive', 'fresh', 'vivid', 'mesmerizing', 'imaginative']
    actions = [
        "invents a spellbinding world that draws you in. ",
        "grabs you and doesn't let go until you've watched the credits roll. ",
        "paints a cinematic picture that makes you feel as though you're right there in the action. ",
        "presents a new take on a familiar genre with a fascinating style. ",
        "demands you perch on the edge of your seat and keep watching until the very last scene. "
    ]

    used_adj = []
    used_action = []

    for movie in movies:
        sel_adj = random.choice(adjectives)
        sel_action = random.choice(actions)

        while sel_adj in used_adj or sel_action in used_action:
            sel_adj = random.choice(adjectives)
            sel_action = random.choice(actions)

        used_adj.append(sel_adj)
        used_action.append(sel_action)

        genre = movie.genre.first().name.lower().replace('&', 'and')

        structure = [
            "{}'s {} new {} movie, {}, {}".format(movie.director, sel_adj, genre, movie.title, sel_action),
            "In the {} {} movie, {}, {} {}".format(sel_adj, genre, movie.title, movie.director, sel_action)
        ]

        sel_struct = random.choice(structure)
        sentences = sentences + sel_struct

    return sentences
</pre>

<pre>
def build_album_sentences(albums):
    sentences = ''
    adjectives = ['exciting', 'finely crafted', 'inventive', 'fresh', 'vivid', 'mesmerizing', 'imaginative']
    actions = [
        "casts a musical spell that draws you in. ",
        "grabs you by the ears and doesn't let go until last note. ",
        "paints a auditory picture that makes you feel as though you're right there on stage. ",
        "presents a new take on a familiar genre with a captivating style. ",
        "demands you crank the stereo to 11 and keep dancing until the very last track. "
    ]

    used_adj = []
    used_action = []

    for album in albums:
        sel_adj = random.choice(adjectives)
        sel_action = random.choice(actions)

        while sel_adj in used_adj or sel_action in used_action:
            sel_adj = random.choice(adjectives)
            sel_action = random.choice(actions)

        used_adj.append(sel_adj)
        used_action.append(sel_action)

        genre = album.genre.first().name.lower().replace('&', 'and')

        structure = [
            "{}'s {} new {} album, {}, {}".format(album.artist, sel_adj, genre, album.title, sel_action),
            "In the {} {} album, {}, {} {}".format(sel_adj, genre, album.title, album.artist, sel_action)
        ]

        sel_struct = random.choice(structure)
        sentences = sentences + sel_struct

    return sentences
</pre>

<p>Our last step is to pass our data to our Django template which we won't cover here. Check out our <a href="https://www.iceorfire.com/weekly-top-picks/">completed page</a>.</p>