11 February 2008

Online games portal in Django - tutorial, part 6

To complete online games portal we must create only several templates. Except base template which is used by all other templates, templates for the website are relatively short and simple. In gamepor folder create templates folder. We are going to insert all templates in this directory. You can also do it on project level if you want.


Below is base.html template content. It will be extended by other templates.


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

<title>{% block title %}Games4All{% endblock %}</title>
<link href="{{ MEDIA_URL }}main.css" rel="stylesheet" type="text/css">

</head>
<body>
<h1>Games4All</h1>
<div id="searchbox">

<form action="/search/" method="get">
<p><label for="sid">Search:</label> <input id="sid" name="s" value="{% block search %}{% endblock %}"></p>

</form>
</div>
<div class="mainbox">
{% block mainbox %}{% endblock %}
</div>
<div class="sidebox">

<div id="categories" class="lbox">
<h2>Categories</h2>
<ul>

{% for category in categories %}
<li><a href="{{ category.get_absolute_url }}">{{ category.list_name }}</a></li>
{% endfor %}
</ul>
</div>

<div id="popsubcats" class="lbox">
<h2>Popular subcategories</h2>
<ul>

{% for subcategory in popular_subcategories %}
<li><a href="{{ subcategory.get_absolute_url }}">{{ subcategory.list_name }}</a></li>
{% endfor %}
</ul>
</div>

</div>
<p id="copyright">This is just a simple demo created by Rafał Jońca and published under GPLv3 license.</p>
</body>
</html>


One word about license. You can use any fragment of above and previous code samples. The GPL license protects the work as a whole, but if you want create own commercial version it will only take you about 15 hours like the one shown here.


Because other templates I'm going to show are shorter and simple to follow (almost self-explanatory), I'll just show their content and postpone of everything.


index.html:


{% extends "base.html" %}

{% block mainbox %}
<div id="newgames" class="gbox">

<h2>New games</h2>
<ol>
{% for game in new_games %}
<li><a href="{{ game.get_absolute_url }}"><img src="{{ game.get_screenshot_url }}" width=120 height=90 alt="Game image"> {{ game.title }}</a></li>

{% endfor %}
</ol>
<p><a href="{% url gampor.views.new_games %}">More new games</a></p>
</div>

<div id="populargames" class="gbox">
<h2>Popular games</h2>
<ol>

{% for game in popular_games %}
<li><a href="{{ game.get_absolute_url }}"><img src="{{ game.get_screenshot_url }}" width=120 height=90 alt="Game image"> {{ game.title }}</a></li>

{% endfor %}
</ol>
<p><a href="{% url gampor.views.popular_games %}">More popular games</a></p>
</div>

{% endblock %}

main_list.html:


{% extends "base.html" %}

{% block title %}{{ subtitle }} | {{ block.super }}{% endblock %}

{% block mainbox %}
<div id="listgames" class="listbox">

<h2>{{ subtitle }}</h2>
{% include "list.html" %}
</div>
{% endblock %}

list.html:



<ol>
{% for object in main_list %}
<li><a href="{{ object.get_absolute_url }}"><img src="{{ object.get_screenshot_url }}" width=120 height=90 alt="Game image"> {{ object }}</a></li>

{% endfor %}
</ol>
{% if is_paginated %}
<p>Page {{ page }} of {{ pages }}</p>
<p id="paging">{% if has_previous %}<a href="?page={{ previous }}{% if searched_name %}&s={{ searched_name|urlencode }}{% endif %}">Previous</a>{% endif %}
{% if has_next and has_previous %} | {% endif %}
{% if has_next %}<a href="?page={{ next }}{% if searched_name %}&s={{ searched_name|urlencode }}{% endif %}">Next</a>{% endif %}
</p>

{% endif %}

search_list.html:


{% extends "base.html" %}

{% block title %}Results for search: {{ searched_name }} | {{ block.super }}{% endblock %}

{% block search %}{{ searched_name }}{% endblock %}

{% block mainbox %}
<div id="listgames" class="listbox">

<h2>Results for search: {{ searched_name }}</h2>
{% if main_list %}
{% include "list.html" %}
{% else %}
<p>No matching results found!</p>
{% endif %}
</div>

{% endblock %}

game.html:


{% extends "base.html" %}

{% block title %}{{ game.title }} | {{ block.super }}{% endblock %}

{% block mainbox %}
<div id="gamebox">
<h2>{{ game.title }}</h2>

{% include type_template %}
<p><b>Rating</b>: {{ game.rating }}</p>
<p><b>In subcategories</b>: {% for object in game.subcategories.all %}<a href="{{ object.get_absolute_url }}">{{ object }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</p>

<p><b>Description</b>: {{ game.description|linebreaksbr }}</p>
<p><b>How to play</b>: {{ game.how_to_play|linebreaksbr }}</p>
</div>

{% endblock %}

flash.html:


<!--[if !IE]> -->
<object id="game" type="application/x-shockwave-flash" data="{{ game.get_local_file_url }}" width={{ game.width }} height={{ game.height }}>

<!-- <![endif]-->
<!--[if IE]>
<object id="game" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0"
width={{ game.width }} height={{ game.height }}>

<param name="movie" value="{{ game.get_local_file_url }}" />
<!--><!-- dummy -->
<param name="loop" value="true" />

</object>
<!-- <![endif]-->

You should also create file similar to the flash.html for two other game types!


As you can see, I'm using two techniques to not write anything twice. First I use inheritance, so all general pages like index.html, main_list.html etc. extends base.html overriding some parts of it. Second, I use including for things that is not suited for inheritance. Of course I can get flash.html to be a subtemplate of game.html but I feel that inclusion is in this case better than extending. For details about using for loop, ifs, filters, inheritance etc., just read the great Django templates doc.


This is the end. If you join all part, you will get fully workable online game portal. I'll prepare a short demo of it and also a download package. This will be available in projects section of the site, but I'm going to put also some summary here.


Because as I see it no one is yet interested in internationalized version and version with static generation, I'll postpone those tutorials to when I get more free time.

10 February 2008

Online games portal in Django - tutorial, part 5

This time we are going to create, views, url configuration for game portal (templates will be in last part). When doing it we will also fix some of earlier files. Lets begin with extending current models.py file. To shorten code, I'm using ... for common parts.

...
from django.conf import settings
import datetime
...

class Category(models.Model):
...

@models.permalink
def get_absolute_url(self):
return ('gampor.views.category_page', (), { 'slug': self.url_name })

class ActiveSubcategoriesManager(models.Manager):
def get_query_set(self):
return super(ActiveSubcategoriesManager, self).get_query_set().filter(category__sites__exact=settings.SITE_ID)

def get_most_popular(self):
from django.db import connection
cursor = connection.cursor()
cursor.execute("""SELECT gs.subcategory_id AS sid, SUM(g.hit_count) as ghc
FROM gampor_game g JOIN gampor_game_subcategories gs ON g.id=gs.game_id
GROUP BY gs.subcategory_id
ORDER BY ghc DESC
LIMIT 10""")
ids = [int(row[0]) for row in cursor.fetchall()]
return self.filter(pk__in=ids)

class Subcategory(models.Model):
...
objects = models.Manager()
active_subcategories = ActiveSubcategoriesManager()
...
@models.permalink
def get_absolute_url(self):
return ('gampor.views.subcategory_page', (), { 'slug': self.url_name })

class ActiveGamesManager(models.Manager):
def get_query_set(self):
return super(ActiveGamesManager, self).get_query_set().filter(subcategories__category__sites__exact=settings.SITE_ID, active=True, publish_date__lte=datetime.date.today())

class Game(models.Model):
...
objects = models.Manager()
active_games = ActiveGamesManager()
...
@models.permalink
def get_absolute_url(self):
if self.game_type == 'E':
return ('gampor.views.ext_redirect', (self.id,))
return ('gampor.views.game_page', (), { 'slug': self.url_name })

I think you will get the general idea what to change. Additions are about two things. First, get_absolute_url() method with decorator enables to create URLs with respect to DRY principle. Using this code the real absolute URL for every model is contained in only one place: urls.py file.

Second, I added two custom managers to limit the data returned for games and subcategories. Using this filtering I can easily get games only active games for current site and with publish date in the past. I'm also using custom SQL to get most popular subcategories but then return them as standard ORM objects. Notice that I'm not using most efficient method (2 queries). We can shrink it to one if we really want.

Add below line to the settings.py file. It shows how to customize pagination.

PAGINATION = 9

Add below lines to the end of main urls.py file to enable application URLs.

urlpatterns += patterns('',
(r'', include('games4all.gampor.urls')),
)

It means that all URLs not matched earlier will go to our application. Now open urls.py from gampor and insert below code with URLs for game portal. This should be self-explanatory.

from django.conf.urls.defaults import *

urlpatterns = patterns('',
(r'^$', 'gampor.views.home'),
(r'^new/$', 'gampor.views.new_games'),
(r'^popular/$', 'gampor.views.popular_games'),
(r'^search/$', 'gampor.views.search_games'),
(r'^category/(?P[\w-]+)/$', 'gampor.views.category_page'),
(r'^subcategory/(?P[\w-]+)/$', 'gampor.views.subcategory_page'),
(r'^game/(?P[\w-]+)/$', 'gampor.views.game_page'),
(r'^external/(\d+)/$', 'gampor.views.ext_redirect'),
)

Every match will call function mentioned at second position. Functions are in views.py which will be our last Python file needed for game portal (we still must create templates). Open it and insert below code.

from models import Category, Subcategory, Game
from django.contrib.sites.models import Site
from django.conf import settings
from django.shortcuts import render_to_response, get_object_or_404
from django.views.generic.list_detail import object_list
from django.views.generic.simple import redirect_to
from django.views.decorators.cache import cache_page
from django.core.cache import cache

TYPE_TO_TEMPLATE = { 'F': 'flash.html', 'S': 'shockwave.html', 'E': 'external.html' }

@cache_page(600)
def home(request):
data = sidebox_data()
data.update({
'new_games': Game.active_games.order_by('-publish_date')[:8],
'popular_games': Game.active_games.order_by('-hit_count')[:8],
})
return render_to_response('index.html', data)

def gampor_object_list(request, queryset, template_name, extra_context={}, empty=False):
extra_context.update(sidebox_data())
return object_list(
request,
queryset = queryset,
template_name = template_name,
template_object_name = "main",
paginate_by = settings.PAGINATION,
allow_empty = empty,
extra_context = extra_context
)

def new_games(request):
return gampor_object_list(request, Game.active_games.order_by('-publish_date'), "main_list.html", { 'subtitle': "New games" })

def popular_games(request):
return gampor_object_list(request, Game.active_games.order_by('-hit_count'), "main_list.html", { 'subtitle': "Popular games" })

def search_games(request):
searched_name = request.REQUEST.get('s', '')
if searched_name != '':
results = Game.active_games.filter(title__contains=searched_name)
else:
results = Game.objects.none()
return gampor_object_list(request, results, "search_list.html", { 'searched_name': searched_name }, True)

def category_page(request, slug):
category = get_object_or_404(Category, url_name=slug)
return gampor_object_list(request, Subcategory.active_subcategories.filter(category=category), "main_list.html", { 'subtitle': category.title_name })

def subcategory_page(request, slug):
subcategory = get_object_or_404(Subcategory, url_name=slug)
return gampor_object_list(request, Game.active_games.filter(subcategories=subcategory), "main_list.html", { 'subtitle': subcategory.title_name })

def game_page(request, slug):
game = get_object_or_404(Game, url_name=slug)
if game.game_type != 'E':
game.hit_count += 1
game.save()
data = sidebox_data()
data.update({
'game': game,
'type_template': TYPE_TO_TEMPLATE[game.game_type],
})
return render_to_response('game.html', data)

def ext_redirect(request, id):
game = get_object_or_404(Game, pk=id , game_type='E')
game.hit_count += 1
game.save()
return redirect_to(request, url=game.url)

# Helper
def sidebox_data(site_id=settings.SITE_ID):
data = cache.get('sidebox_data_%d' % site_id)
if data == None:
data = {
'MEDIA_URL': settings.MEDIA_URL,
'categories': Category.objects.filter(sites__exact=site_id),
'popular_subcategories': Subcategory.active_subcategories.get_most_popular(),
}
cache.set('sidebox_data_%d' % site_id, data, 1200)
return data

It is really hard to explain everything going on here (but the code is rather simple), so I'm only going to say what I'm using and you should look into details at Django Documentation:

  • caching for home page and sidebox
  • generic list view with own wrapper
  • get_object_or_404 for getting object from db or return 404 code
  • incrementing hits and redirect

The only thing we still need are templates, but this must wait to next post...

Online games portal in Django - tutorial, part 4

In fourth part I'm going to show very simple online games portal design and also show basic HTML i CSS for it. I'm not going to comment everything because it is not a tutorial about XHTML or CSS. Please remember that the CSS and HTML is web standard compliant but it is very simple - I'm using only one CSS file with no presentational images so the site looks very badly, but because it has good hooks, I think anyone with better graphical skills will get it look better :)

First, I'm going to talk about general site structure:

  • two columns: main and sidebox
  • search and site name at top
  • sidebox on right containing: list of all categories and list of several most popular subcategories (sum of hits of their games)
  • main content: index (8 new games, 8 popular), search (results, paginated), new games (list, paginated), popular games (list, paginated), subcategories for category (list, paginated), games for subcategory (list, paginated), game page (three versions depending on game type)

Below is the index.html as HTML mock-up:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Games4All</title>
<link href="main.css" rel="stylesheet" type="text/css">

</head>
<body>
<h1>Games4All</h1>
<div id="searchbox">

<form action="/search/" method="get">
<p><label for="sid">Search:</label> <input id="sid" name="s" value=""></p>

</form>
</div>
<div class="mainbox">
<div id="newgames" class="gbox">

<h2>New games</h2>
<ol>
<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>

<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>

<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>

<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>

<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>

<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>

<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>
<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>

</ol>
<p><a href="/new/">More new games</a></p>
</div>
<div id="populargames" class="gbox">

<h2>Popular games</h2>
<ol>
<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>

<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>

<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>

<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>

<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>

<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>

<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>

<li><a href="/game/test"><img src="img.gif" width=120 height=90 alt="Game image"> Game A</a></li>
</ol>

<p><a href="/new/">More popular games</a></p>
</div>
</div>
<div class="sidebox">

<div id="categories" class="lbox">
<h2>Categories</h2>
<ul>

<li><a href="/category/A">Action</a></li>
<li><a href="/category/A">Action</a></li>

<li><a href="/category/A">Action</a></li>
<li><a href="/category/A">Action</a></li>

<li><a href="/category/A">Action</a></li>
<li><a href="/category/A">Action</a></li>

<li><a href="/category/A">Action</a></li>
</ul>
</div>
<div id="popsubcats" class="lbox">

<h2>Popular subcategories</h2>
<ul>
<li><a href="/subcategory/A">Action One</a></li>

<li><a href="/subcategory/A">Action One</a></li>
<li><a href="/subcategory/A">Action One</a></li>

<li><a href="/subcategory/A">Action One</a></li>
<li><a href="/subcategory/A">Action One</a></li>

<li><a href="/subcategory/A">Action One</a></li>
</ul>
</div>
</div>

<p id="copyright">This is just a simple demo created by Rafał Jońca and published under GPLv3 license.</p>
</body>
</html>

Below is the CSS file for mock-up containing also CSS for other parts of site. Save the CSS file in public_html folder as main.css.

html { background-color: blue;}
body { width: 760px; margin: 0 auto; background-color: white; font-family: Arial, Helvetica, sans-serif; font-size: 12px; padding: 0; position: relative;}
h1, h2, p { margin: 0 1em 1ex 1em; padding: 1ex 0;}
h1 { font-size: 180%; }
h2 { font-size: 140%; }
#searchbox { position: absolute; right: 1em; top: 1em}
#searchbox input { width: 130px;}
p#copyright { text-align: center; clear: both; border-top: 1px dashed gray; padding-top: 5px; }
.mainbox { width: 540px; float: left; border-right: 1px dashed gray; padding-right: 10px; }
.gbox p { text-align: right; padding-right: 1em; clear: both; }
.sidebox { width: 200px; float: right;}
ul, ol { list-style-type: none; padding-left: 2em; margin-left: 0;}
.gbox ol { padding-left: 15px; margin-left: 0 }
.gbox li { width: 120px; height: 125px; padding-right: 10px; padding-left: 0; margin-bottom: 10px; float: left;}
img { border: 0; }
.gbox a, .listbox a { text-decoration: none; }
.gbox p a, .listbox p a { text-decoration: underline; }
#gamebox { text-align: center; }
#gamebox p { text-align: left; }
.listbox ol { padding-left: 45px; margin-left: 0; }
.listbox li { width: 120px; height: 125px; padding-right: 45px; padding-left: 0; margin-bottom: 20px; float: left;}
.listbox p { text-align: left; padding-left: 1em; clear: both; margin-top: 0; margin-bottom: 0;}
.listbox p#paging { text-align: right; padding-right: 1em; clear: both; margin-bottom: 5px; }

Other part of the system, list page and game page do not differ much, so I'll show them in one of the next entries as ready Django templates. Save both files and you can see how main page of portal will look like. Yep, it looks bad but it fully functional (HTML part) and you just have to use more polished CSS :)

Online games portal in Django - tutorial, part 3

Today we are going to populate the database created last time (part 2). For this I'll create small script that is not very clean and optimized, but it works and was created in less than hour. It is a good lesson of Django model API, but do not use it to learn Python :)

I'll show the code first and then comment the most important parts.

#!/usr/bin/env python

# Configuration of db population system.
# It will create about 1000 games, 100 subcategories and 8 categories for defined sites.
SITE_ID = (2,)
CATEGORIES = ('Action', 'Platform', 'Kids', 'Sport', 'Simulation', 'Puzzle', 'Card', 'Board',)
SUBCATEGORIES = ('One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Eleven', 'Twelve', 'Thirteen')
GAME_IMGS = ('_init/A.gif', '_init/B.gif', '_init/C.gif')
SUB_IMGS = ('_init/S1.gif', '_init/S2.gif')
GAME_DATA = ('_init/game.swf', '_init/game.dcr', 'http://jonca.vdl.pl/')
GAMES_IN_SUB_MAX = 20
CHARS = 'abcdefghijklmnoprstuwyz'

# Hack to properly initialize Django ORM
import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'games4all.settings'

import time
from datetime import date
from random import randint, choice
from django.contrib.sites.models import Site
from games4all.gampor.models import Category, Subcategory, Game, GAME_TYPES

def name_generator(chars=10, variance=5, words=1, multiline=False):
""" Generate random texts. """

def word(length):
r = [choice(CHARS) for x in range(0, length)]
r.append(' ')
return ''.join(r)

result = []
for i in range(0, words):
result.append(randint(chars-variance,chars+variance))
result = [word(x) for x in result]

result[0] = result[0].title()
result[-1] = result[-1].strip()
if words > 1 and multiline:
result[0] = result[0].strip()
result.insert(1, '\n')

return ''.join(result)

def populate_categories(site_id):
""" Create categories for site with passed id. """

s = Site.objects.get(id=site_id)
for cat in CATEGORIES:
c = Category(url_name=cat.lower(), list_name=cat, title_name=(cat+' games'))
c.save()
c.sites.add(s)
print cat
populate_subcategories(c)

def populate_subcategories(c):
""" Create subcategories for passed category object. """

for scat in SUBCATEGORIES:
scatn = c.list_name+' '+scat
scatu=c.url_name+'-'+scat.lower()
s = Subcategory(url_name=scatu, list_name=scatn, title_name=(scatn+' games'))
s.category = c
s.save_screenshot_file(scatu+'.gif',file(choice(SUB_IMGS),'rb').read())
s.save()
print '.',
populate_games(s)

def populate_games(s):
""" Create games for passed subcategory object. """

for i in range(0, randint(1, GAMES_IN_SUB_MAX)):
name = name_generator(words=3)
uname=name.lower().replace(' ','-')
type = randint(0,2)
g = Game(
title=name,
url_name=uname,
active=True,
game_type=GAME_TYPES[type][0],
description=name_generator(words=randint(7,25)),
how_to_play=name_generator(words=randint(7,25),multiline=True),
rating = randint(0,5),
hit_count = randint(0,500)
)
t = time.localtime(time.time()+randint(-10000000,1000000))
g.publish_date = date(t[0],t[1],t[2])
g.save()
if 0 <= type < 2:
g.width = randint(400,500)
g.height = randint(300,400)
if type == 0:
g.save_local_file_file(uname+'.swf',file(GAME_DATA[0],'rb').read())
else:
g.save_local_file_file(uname+'.dcr',file(GAME_DATA[1],'rb').read())
else:
g.url = GAME_DATA[2]
g.save_screenshot_file(uname+'.gif',file(choice(GAME_IMGS),'rb').read())
g.subcategories.add(s)

# Start population of site if run from command line.
if __name__ == '__main__':
for site_id in SITE_ID:
populate_categories(site_id)

You can run the script with following command, but you may have to set PYTHONPATH to folder above games4all to get it work properly.

python populatedb.py

The populating may take some time because it not only inserts database records, but also copies image files in rather not very optimal way.

Lets back to the code. First part contains simple configuration with site id (use id you created in admin tool), names to use for categories and subcategories, and also names of image files etc. for use in generation.

Below configuration we are importing modules but the first thing to do is to set DJANGO_SETTINGS_MODULE to proper value so Django ORM can find the database configuration.

The name_generator() is very simple and stupid function that generate texts with variable word length. It will be used for game titles, game descriptions and game how to play instructions.

The populate_categories() takes site id and then creates all categories and links them with site. For each category it also calls populate_subcategories() to create subcategories. Please look that we have to save category c.save() and then link it to site using c.sites.add(s). The constructor takes most of values.

The populate_subcategories() works very similar to his category brother, but because it uses 1:N with categories, we can simply use s.category = c to link subcategory to category. The s.save_screenshot_file line saves the subcategory screenshot by loading it first and setting the name that is the same as subcategory url name. In this example you may not see it, but s.save() is not really necessary! Django saves new object automatically with save_screenshot_file() call, which seems strange but is reasonable. Writing file in file system when object is not saved and may never be, makes no sense. You must remember about it because you get save error if required fields won't be set.

The last function, populate_games() is the most complicated of the three but only because it has more data to save. The general idea stays the same. Please also look that we are setting different fields for external games (url) and different for Flash and Shockwave games (file, width, height). Notice that this time I'm saving game object before starting to save files for them and assigning to subcategories.

The very last part of file just starts the whole process if we run the module as the main script.

After running the script you may log into admin panel and see the new data. Next tutorial part will be about CSS and HTML for our simple game portal.

3 February 2008

Online games portal in Django - tutorial, part 2

Today we are going to concentrate on creating data model for game portal. Requirements:

  • we want to run several sites with the same or similar data, for example general portal or specialized portal for girls
  • the difference will be in used categories, so we link them with sites
  • to get better user experience (faster looking for interesting games), we will create three levels: categories, subcategories and games
  • subcategory can be connected to only one category but game can be in many subcategories (to get things harder)
  • game can be of three different types (Flash, Shockwave or external). External behaves differently than two first because it is just a link to other site (once more to get things harder)
  • we will use screenshots for subcategories and games, ratings (but not user editable) and hit counter to measure popularity

This should suffice as an introduction. Go to games4all folder. Last time we created there new application named gampor. Look into this folder and then open models.py file. The file is going to contain our model and admin configuration data. It is almost empty, but we are going to change that in a second. Add below code to it:

from django.db import models
from django.contrib.sites.models import Site

GAME_TYPES = (
('F', 'Flash'),
('S', 'Shockwave'),
('E', 'External'),
)

We are importing base model classes and also Site model that we will use with categories. The GAME_TYPES tuple contains choices for game types for use in admin and also in the future in templates.

The next step will be creating category model. It is rather simple, but we want to have connections to many sites and also want to use pretty urls. Here it is:

class Category(models.Model):
url_name = models.SlugField(prepopulate_from=('list_name',), unique=True) # For pretty urls
list_name = models.CharField(max_length=60, unique=True) # Cat name in lists
title_name = models.CharField(max_length=60) # Cat name in titles
sites = models.ManyToManyField(Site, null=True, blank=True) # M-N linkage to sites

class Meta:
ordering = ('list_name',) # Default ordering
verbose_name_plural = 'categories' # To get proper name in admin panel

class Admin:
list_display = ('list_name',) # What to display on admin list
search_fields = ('list_name',) # What fields should be searchable
list_filter = ('sites',) # By what we want to filter categories in admin

# Defines nice string value for category (used by admin panel)
def __unicode__(self):
return self.list_name

I'm not going to explain here all the details of model classes except commented parts. For this just go to Django model doc.

The next thing is to define subcategories. They are very similar to categories except that they use one to many linkage and have screenshot field. Here is the code:

class Subcategory(models.Model):
url_name = models.SlugField(prepopulate_from=('list_name',), unique=True)
list_name = models.CharField(max_length=60)
title_name = models.CharField(max_length=60)
category = models.ForeignKey(Category, null=True, blank=True) # 1-N link with category.
# Field for screenshots that support uploading to folder
screenshot = models.ImageField(upload_to='img/b/%Y', help_text='Preferred size: 120x90px')

class Meta:
ordering = ('list_name',)
verbose_name_plural = 'subcategories'

class Admin:
list_display = ('list_name','category')
search_fields = ('list_name',)
list_filter = ('category',)

def __unicode__(self):
return self.list_name

Not that hard! Notice that we can create subcategory without assigning it to category so effectively we can prepare many categories and do not show them if we do not want to :)

Last thing is game model which will be the most complicated one, but as you will see we are going to use many already known elements. First of all there will be game_type field with limited choice of game types, url field with external game URL, publish_date field with publication date what we will use for ordering and showing only game with publish date in the past. Also please look at more complicated Admin class because we want to modify the edit view in admin panel.

class Game(models.Model):
title = models.CharField(max_length=60, unique=True)
url_name = models.SlugField(prepopulate_from=('title',), unique=True)
active = models.BooleanField() # To temporary disabling the game
subcategories = models.ManyToManyField(Subcategory, null=True, blank=True, related_name='games_in', filter_interface=models.HORIZONTAL)

screenshot = models.ImageField(upload_to='img/s/%Y', help_text='Preferred size: 120x90px')
game_type = models.CharField(max_length=1, choices=GAME_TYPES) # Limited game types
width = models.PositiveSmallIntegerField(null=True, blank=True)
height = models.PositiveSmallIntegerField(null=True, blank=True)
local_file = models.FileField(null=True, blank=True, upload_to='data/%Y')
url = models.URLField(null=True, blank=True)
description = models.TextField()
how_to_play = models.TextField()
rating = models.PositiveSmallIntegerField()

publish_date = models.DateField()
hit_count = models.PositiveIntegerField()

class Meta:
ordering = ('-publish_date','title')
get_latest_by = 'publish_date'

class Admin:
list_display = ('title','publish_date','game_type','active')
search_fields = ('title','url_name')
# What field to use for additional filtering by date
date_hierarchy = 'publish_date'
list_filter = ('active','subcategories')
# This elements gives us great control over edit page design
fields = (
(None, {
'fields': ('url_name', 'title', 'active')
}),
('Game details', {
'classes': 'collapse',
'fields' : ('screenshot', 'game_type', ('width', 'height'), 'local_file', 'url', 'description', 'how_to_play', 'rating')
}),
('Publishing details', {
'classes': 'collapse',
'fields' : ('publish_date', 'hit_count')
}),
)

def __unicode__(self):
return self.title

If you need more explanation about some field types, look into Django model documentation.

That's it! Simple data model is ready. To test it, run python manage.py syncdb and then start development server with python manage.py runserver 8080. Test your model using admin panel.

Within next few days I'm going to show how to use model API to populate database with dummy data for use in future tests.