├── __init__.py
├── dates
├── __init__.py
├── admin.py
├── samples.py
└── models.py
├── sports
├── __init__.py
├── templates
│ └── admin
│ │ └── sports
│ │ └── team
│ │ └── change_form.html
├── admin.py
├── sample.py
└── models.py
├── .gitignore
├── slides
├── modeling-challenges.odp
└── modeling-challenges.pdf
├── main_urls.py
├── manage.py
├── settings.py
├── LICENSE.txt
├── fixtures
├── dates.json
├── sports.json
└── initial_data.json
└── README.rst
/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dates/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sports/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.swp
3 | *.sqlite
4 | .~*
5 |
--------------------------------------------------------------------------------
/slides/modeling-challenges.odp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/malcolmt/django-modeling-examples/HEAD/slides/modeling-challenges.odp
--------------------------------------------------------------------------------
/slides/modeling-challenges.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/malcolmt/django-modeling-examples/HEAD/slides/modeling-challenges.pdf
--------------------------------------------------------------------------------
/dates/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib.admin import site
2 |
3 | from dates import models
4 |
5 |
6 | site.register([models.Date, models.DateRange])
7 |
8 |
--------------------------------------------------------------------------------
/main_urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import * # pylint: disable-msg=W0401,W0614
2 | from django.contrib import admin
3 |
4 | admin.autodiscover()
5 |
6 | urlpatterns = patterns('',
7 | (r'^admin/', include(admin.site.urls)),
8 | )
9 |
10 |
--------------------------------------------------------------------------------
/sports/templates/admin/sports/team/change_form.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/change_form.html" %}
2 |
3 | {% block after_related_objects %}
4 |
Current players
5 | {% for player in original.current_players %}
6 | - {{ player }}
7 | {% empty %}
8 | - (None)
9 | {% endfor %}
10 |
11 | Current coaches
12 | {% for coach in original.current_coaches %}
13 | - {{ coach }}
14 | {% empty %}
15 | - (None)
16 | {% endfor %}
17 | {% endblock %}
18 |
19 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from django.core.management import execute_manager
3 | try:
4 | import settings # Assumed to be in the same directory.
5 | except ImportError:
6 | import sys
7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
8 | sys.exit(1)
9 |
10 | if __name__ == "__main__":
11 | execute_manager(settings)
12 |
--------------------------------------------------------------------------------
/sports/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from sports import models
4 |
5 | class TeamAdmin(admin.ModelAdmin):
6 | """
7 | Display current members and coaches on the Team edit form.
8 | """
9 | #def render_change_form(self, request, context, add=False, change=False,
10 | # form_url="", obj=None):
11 | # if not change:
12 | # return super(TeamAdmin, self).render_change_form(request, context,
13 | # add, change, form_url, obj)
14 | # context["current_players"] = obj.current_players()
15 | # context["current_coaches"] = obj.current_coaches()
16 |
17 | admin.site.register(models.Team, TeamAdmin)
18 | admin.site.register([models.Person, models.League, models.LeagueTeam,
19 | models.TeamMember, models.LeagueUmpire])
20 |
21 |
--------------------------------------------------------------------------------
/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | PROJ_ROOT = os.path.abspath(os.path.dirname(__file__))
4 | DEV_MODE = True # Used to control local static content serving.
5 |
6 | DEBUG = True
7 | TEMPLATE_DEBUG = DEBUG
8 | ADMINS = ()
9 | MANAGERS = ADMINS
10 |
11 | DATABASES = {
12 | 'default': {
13 | 'ENGINE': "django.db.backends.sqlite3",
14 | 'NAME': os.path.join(PROJ_ROOT, "models.sqlite"),
15 | }
16 | }
17 | TIME_ZONE = None
18 | LANGUAGE_CODE = 'en-us'
19 | USE_I18N = True
20 | USE_L10N = True
21 |
22 | MEDIA_ROOT = ''
23 | MEDIA_URL = '/static/'
24 | ADMIN_MEDIA_PREFIX = '/media/'
25 |
26 | SECRET_KEY = '(okqqmuqmi_%10@ob3jn&@@s-qo(lnz9x0w=rc_9z)4jz0y+tl'
27 |
28 | TEMPLATE_LOADERS = (
29 | 'django.template.loaders.filesystem.Loader',
30 | 'django.template.loaders.app_directories.Loader',
31 | )
32 |
33 | MIDDLEWARE_CLASSES = (
34 | 'django.middleware.common.CommonMiddleware',
35 | 'django.contrib.sessions.middleware.SessionMiddleware',
36 | 'django.middleware.csrf.CsrfViewMiddleware',
37 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
38 | 'django.contrib.messages.middleware.MessageMiddleware',
39 | )
40 |
41 | ROOT_URLCONF = 'main_urls'
42 | TEMPLATE_DIRS = (
43 | )
44 |
45 | INSTALLED_APPS = (
46 | 'django.contrib.auth',
47 | 'django.contrib.contenttypes',
48 | 'django.contrib.sessions',
49 | 'django.contrib.messages',
50 | 'django.contrib.admin',
51 | 'dates',
52 | 'sports',
53 | )
54 |
55 | FIXTURE_DIRS = (
56 | os.path.join(PROJ_ROOT, "fixtures"),
57 | )
58 |
59 |
--------------------------------------------------------------------------------
/dates/samples.py:
--------------------------------------------------------------------------------
1 | """
2 | Some sample date data to use for experimenting with the Date and DateRange
3 | models.
4 |
5 | This data is loaded as part of an initial fixture if you ran "syncdb", but the
6 | code is included here in order to regenerate things from scratch.
7 | """
8 |
9 | from datetime import date, datetime
10 |
11 | from dates import models
12 |
13 |
14 | # All dates are in DD/MM/YYYY format. North Americans will have to mentally
15 | # convert.
16 | DATES = (
17 | ("01/11/1937", 0), # precise
18 | ("01/08/1891", 1), # month
19 | ("11/11/1975", 2), # year
20 | ("5/4/1940", 3), # decade
21 | ("1/1/1000", 4), # century
22 | )
23 |
24 | DATE_RANGES = (
25 | ("1/1/0101", 4, "1/1/0400", 4),
26 | ("2/2/1201", 4, "30/6/1752", 3),
27 | ("1/1/1905", 3, "8/9/2010", 0),
28 | )
29 |
30 | OPEN_RANGES = (
31 | ("2/2/1201", 4),
32 | ("1/1/1905", 3),
33 | )
34 |
35 | def load_samples():
36 | for date_str, prec in DATES:
37 | date = datetime.strptime(date_str, "%d/%m/%Y").date()
38 | models.Date(date=date, precision=prec).save()
39 |
40 | for date1_str, prec1, date2_str, prec2 in DATE_RANGES:
41 | date1 = datetime.strptime(date1_str, "%d/%m/%Y").date()
42 | date2 = datetime.strptime(date2_str, "%d/%m/%Y").date()
43 | obj1 = models.Date.objects.create(date=date1, precision=prec1)
44 | obj2 = models.Date.objects.create(date=date2, precision=prec2)
45 | models.DateRange(start=obj1, end=obj2).save()
46 |
47 | for date_str, prec in OPEN_RANGES:
48 | date = datetime.strptime(date_str, "%d/%m/%Y").date()
49 | obj = models.Date.objects.create(date=date, precision=prec)
50 | models.DateRange(start=obj).save()
51 |
52 | if __name__ == "__main__":
53 | load_samples()
54 |
55 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | All code in this package is licensed as below. This is the standard "new BSD"
2 | license (see http://www.opensource.org/licenses/bsd-license.php), the same
3 | license that is used for Django itself.
4 |
5 | --o----------o--
6 |
7 | Original code by Malcolm Tredinnick is licensed as:
8 |
9 | Copyright (c) 2010, Malcolm Tredinnick
10 | All rights reserved.
11 |
12 | Redistribution and use in source and binary forms, with or without
13 | modification, are permitted provided that the following conditions are met:
14 |
15 | * Redistributions of source code must retain the above copyright notice,
16 | this list of conditions and the following disclaimer.
17 | * Redistributions in binary form must reproduce the above copyright notice,
18 | this list of conditions and the following disclaimer in the documentation
19 | and/or other materials provided with the distribution.
20 | * The name of Malcolm Tredinnick may not be used to endorse or promote
21 | products derived from this software without specific prior written
22 | permission.
23 |
24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
28 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
30 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
31 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
32 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 |
35 |
--------------------------------------------------------------------------------
/sports/sample.py:
--------------------------------------------------------------------------------
1 | """
2 | Creates some sample data to illustrate the sports models. This is also
3 | available as a fixture (generated via this file).
4 | """
5 |
6 | from datetime import date
7 |
8 | from sports import models
9 |
10 |
11 | PEOPLE = (
12 | "Fred Flintstone",
13 | "Barney Rubble",
14 | "Bam-Bam",
15 | "Pebbles",
16 | "Dino",
17 | )
18 |
19 | TEAMS = (
20 | "Bedrock Bumblebees",
21 | "Whackity-sacks",
22 | )
23 |
24 | LEAGUES = (
25 | "Premier",
26 | "Old-timers",
27 | )
28 |
29 | def load_samples():
30 | people = {}
31 | teams = {}
32 | leagues = {}
33 | for name in PEOPLE:
34 | obj = models.Person.objects.create(name=name)
35 | people[name] = obj
36 | for name in TEAMS:
37 | obj = models.Team.objects.create(name=name)
38 | teams[name] = obj
39 | for name in LEAGUES:
40 | obj = models.League.objects.create(name=name)
41 | leagues[name] = obj
42 |
43 | start = date(2000, 4, 12)
44 | end = date(2002, 6, 30)
45 |
46 | # Leagues
47 | models.LeagueUmpire(joined=start, departed=end,
48 | league=leagues["Old-timers"], umpire=people["Dino"]).save()
49 | models.LeagueTeam(joined=start, departed=end, league=leagues["Old-timers"],
50 | team=teams["Whackity-sacks"]).save()
51 | start = date(2002, 7, 1)
52 |
53 | # Umpires
54 | models.LeagueUmpire(joined=start,
55 | league=leagues["Premier"], umpire=people["Dino"]).save()
56 |
57 | # Teams in Leagues
58 | for team in ("Whackity-sacks", "Bedrock Bumblebees"):
59 | models.LeagueTeam(joined=start, league=leagues["Premier"],
60 | team=teams[team]).save()
61 |
62 | # Players in teams
63 | team = teams["Whackity-sacks"]
64 | for player in ("Fred Flintstone", "Barney Rubble"):
65 | models.TeamMember(joined=start, team=team, person=people[player],
66 | role=models.PLAYER).save()
67 |
68 | # Team coach
69 | models.TeamMember(joined=start, team=team, person=people["Pebbles"],
70 | role=models.COACH).save()
71 |
72 | if __name__ == "__main__":
73 | load_samples()
74 |
75 |
--------------------------------------------------------------------------------
/sports/models.py:
--------------------------------------------------------------------------------
1 | """
2 | In many sports, a person can play one (or more) of multiple roles: player,
3 | coach or umpire/referee, for example.
4 |
5 | For our purposes here, an umpire is associated with a league (of which there
6 | can be more than one), whilst players and coaches are associated with teams,
7 | which make up the leagues. A single person can have multiple roles over time,
8 | sometimes more than one at a time (e.g. player-coach).
9 | """
10 |
11 | from django.db import models
12 |
13 |
14 | COACH = "C"
15 | PLAYER = "P"
16 |
17 | class Person(models.Model):
18 | name = models.CharField(max_length=100)
19 |
20 | class Meta:
21 | verbose_name_plural = "people"
22 |
23 | def __unicode__(self):
24 | return self.name
25 |
26 |
27 | class Team(models.Model):
28 | name = models.CharField(max_length=100)
29 |
30 | def __unicode__(self):
31 | return self.name
32 |
33 | def current_coaches(self):
34 | return Person.objects.filter(teammember__team=self,
35 | teammember__role=COACH, teammember__departed=None). \
36 | order_by("name")
37 |
38 | def current_players(self):
39 | return Person.objects.filter(teammember__team=self,
40 | teammember__role=PLAYER, teammember__departed=None). \
41 | order_by("name")
42 |
43 |
44 | class League(models.Model):
45 | name = models.CharField(max_length=100)
46 | umpires = models.ManyToManyField(Person, through="LeagueUmpire")
47 | teams = models.ManyToManyField(Team, through="LeagueTeam")
48 |
49 | def __unicode__(self):
50 | return self.name
51 |
52 |
53 | class Membership(models.Model):
54 | """
55 | A specification of belonging to something for a period of time. Concrete
56 | base classes with supply the "somethings".
57 | """
58 | joined = models.DateField()
59 | departed = models.DateField(null=True, blank=True)
60 |
61 | class Meta:
62 | abstract = True
63 |
64 | def _to_string(self, lhs, rhs):
65 | pairing = u"%s - %s" % (lhs, rhs)
66 | if self.departed:
67 | return u"%s (%s - %s)" % (pairing,
68 | self.joined.strftime("%d %b %Y"),
69 | self.departed.strftime("%d %b %Y"))
70 | return u"%s (%s - )" % (pairing, self.joined.strftime("%d %b %Y"))
71 |
72 |
73 | class LeagueMembership(Membership):
74 | league = models.ForeignKey(League)
75 |
76 | class Meta:
77 | abstract = True
78 |
79 | def _to_string(self, lhs):
80 | return super(LeagueMembership, self)._to_string(lhs, self.league)
81 |
82 |
83 | class LeagueTeam(LeagueMembership):
84 | team = models.ForeignKey(Team)
85 |
86 | def __unicode__(self):
87 | return self._to_string(self.team)
88 |
89 |
90 | class LeagueUmpire(LeagueMembership):
91 | umpire = models.ForeignKey(Person)
92 |
93 | def __unicode__(self):
94 | return self._to_string(self.umpire)
95 |
96 |
97 | class TeamMember(Membership):
98 | team = models.ForeignKey(Team, related_name="members")
99 | person = models.ForeignKey(Person)
100 | role = models.CharField(max_length=2,
101 | choices=((COACH, "coach"), (PLAYER, "player")))
102 |
103 | def __unicode__(self):
104 | return self._to_string(self.team, self.person)
105 |
106 |
--------------------------------------------------------------------------------
/dates/models.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | """
3 | The Challenge: Create a useful way of modeling dates — both a single point in
4 | time and a start and end point — that have varying levels of precision
5 | attached. The date might be trying to indicate anything from a specific day, or
6 | a whole year, or an entire century.
7 |
8 | We aren't going to deal with the problem of modeling error bars on dates (such
9 | as 1753 ± 18 years), although similar techniques could be used for that case.
10 | One date model is not appropriate for every single situation.
11 | """
12 |
13 | import datetime
14 |
15 | from django.db import models
16 |
17 |
18 | PRECISION_CHOICES = (
19 | (0, "precise"),
20 | (1, "month"),
21 | (2, "year"),
22 | (3, "decade"),
23 | (4, "century"),
24 | )
25 |
26 | class Date(models.Model):
27 | """
28 | Dates with precision measurements. This class is a little naïve when it
29 | comes to really ancient dates: it doesn't take calendar changes into
30 | consideration. Every year has 365 days, for example (if you think that's
31 | a given, look at September, 1752 when you have a spare moment).
32 | """
33 | date = models.DateField()
34 | precision = models.IntegerField(default=0, choices=PRECISION_CHOICES)
35 |
36 | def __unicode__(self):
37 | """
38 | An intentionally naïve display of the relevant data. Most displays of
39 | dates will want to format things differently to this, but we'll leave
40 | the specifics to utility functions and focus on genericity in this
41 | method.
42 | """
43 | # XXX: Work around fact that strftime() cannot usually handle years
44 | # prior to 1900.
45 | tmp_date = self.date.replace(year=1900)
46 | date_str = u"%s, %s" % (tmp_date.strftime("%d %b"), self.date.year)
47 | if self.precision == 0:
48 | return date_str
49 | return u"%s containing %s" % (self.get_precision_display(), date_str)
50 |
51 | def canonical_version(self):
52 | """
53 | Returns a canoical version of the date. Useful for sorting and
54 | comparisons. This is earliest date in the interval.
55 |
56 | For example, 1/1/1903 and 6/6/1901 with decade precisions both have a
57 | canonical version of 1/1/1901 (with decade precision).
58 |
59 | Centuries and decades are both treated as starting on the year ending
60 | with "1" (e.g. 1901, rather than 1900).
61 | """
62 | precision = self.precision
63 | if precision == 0:
64 | return self.date
65 | if precision == 1:
66 | return datetime.date(1, self.date.month, self.date.year)
67 | if precision == 2:
68 | return datetime.date(1, 1, self.date.year)
69 | if precision == 3:
70 | new_year = 1 + 10 * ((self.date.year - 1) / 10)
71 | return datetime.date(1, 1, new_year)
72 | if precision == 4:
73 | new_year = 1 + 100 * ((self.date.year - 1) / 100)
74 | return datetime.date(1, 1, new_year)
75 | raise AssertionError("Bad data: should never have gotten here!")
76 |
77 | class DateRange(models.Model):
78 | start = models.ForeignKey(Date, related_name="start_dates")
79 | end = models.ForeignKey(Date, null=True, related_name="end_dates")
80 |
81 | def __unicode__(self):
82 | if self.end:
83 | return u"%s to %s" % (self.start, self.end)
84 | return u"range starting %s" % self.start
85 |
86 |
--------------------------------------------------------------------------------
/fixtures/dates.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "fields": {
4 | "date": "1937-11-01",
5 | "precision": 0
6 | },
7 | "model": "dates.date",
8 | "pk": 1
9 | },
10 | {
11 | "fields": {
12 | "date": "1891-08-01",
13 | "precision": 1
14 | },
15 | "model": "dates.date",
16 | "pk": 2
17 | },
18 | {
19 | "fields": {
20 | "date": "1975-11-11",
21 | "precision": 2
22 | },
23 | "model": "dates.date",
24 | "pk": 3
25 | },
26 | {
27 | "fields": {
28 | "date": "1940-04-05",
29 | "precision": 3
30 | },
31 | "model": "dates.date",
32 | "pk": 4
33 | },
34 | {
35 | "fields": {
36 | "date": "1000-01-01",
37 | "precision": 4
38 | },
39 | "model": "dates.date",
40 | "pk": 5
41 | },
42 | {
43 | "fields": {
44 | "date": "0101-01-01",
45 | "precision": 4
46 | },
47 | "model": "dates.date",
48 | "pk": 6
49 | },
50 | {
51 | "fields": {
52 | "date": "0400-01-01",
53 | "precision": 4
54 | },
55 | "model": "dates.date",
56 | "pk": 7
57 | },
58 | {
59 | "fields": {
60 | "date": "1201-02-02",
61 | "precision": 4
62 | },
63 | "model": "dates.date",
64 | "pk": 8
65 | },
66 | {
67 | "fields": {
68 | "date": "1752-06-30",
69 | "precision": 3
70 | },
71 | "model": "dates.date",
72 | "pk": 9
73 | },
74 | {
75 | "fields": {
76 | "date": "1905-01-01",
77 | "precision": 3
78 | },
79 | "model": "dates.date",
80 | "pk": 10
81 | },
82 | {
83 | "fields": {
84 | "date": "2010-09-08",
85 | "precision": 0
86 | },
87 | "model": "dates.date",
88 | "pk": 11
89 | },
90 | {
91 | "fields": {
92 | "date": "1201-02-02",
93 | "precision": 4
94 | },
95 | "model": "dates.date",
96 | "pk": 12
97 | },
98 | {
99 | "fields": {
100 | "date": "1905-01-01",
101 | "precision": 3
102 | },
103 | "model": "dates.date",
104 | "pk": 13
105 | },
106 | {
107 | "fields": {
108 | "end": 7,
109 | "start": 6
110 | },
111 | "model": "dates.daterange",
112 | "pk": 1
113 | },
114 | {
115 | "fields": {
116 | "end": 9,
117 | "start": 8
118 | },
119 | "model": "dates.daterange",
120 | "pk": 2
121 | },
122 | {
123 | "fields": {
124 | "end": 11,
125 | "start": 10
126 | },
127 | "model": "dates.daterange",
128 | "pk": 3
129 | },
130 | {
131 | "fields": {
132 | "end": null,
133 | "start": 12
134 | },
135 | "model": "dates.daterange",
136 | "pk": 4
137 | },
138 | {
139 | "fields": {
140 | "end": null,
141 | "start": 13
142 | },
143 | "model": "dates.daterange",
144 | "pk": 5
145 | }
146 | ]
147 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ==============================
2 | Modeling Challenges In Django
3 | ==============================
4 |
5 | Supporting code and slides for a talk originally given at DjangoCon-US,
6 | September 2010 (in Portland, Oregon, USA).
7 |
8 | Short Description
9 | ==================
10 |
11 | How would you model players, umpires and coaches in baseball data when the same
12 | person can switch roles over the course of their life? How about servers in
13 | racks with power boards attached (and cords running across the room to remote
14 | boards)? Here is one approach to create minimal and well-performing models for
15 | such real-life situations.
16 |
17 | Abstract
18 | =========
19 |
20 | The slightly over-simplified but useful rule of thumb when creating database
21 | schema is “normalize until it hurts, [then] denormalize until it works.” If
22 | only people didn’t skip the first step so often. Using a data modeling layer,
23 | such as Django's models, doesn't absolve the system architects from the need to
24 | create good design. It also doesn't require them to do so, since you can get
25 | away with a lot of sub-optimality with many data sets.
26 |
27 | The real difficulty here, though, is that the trade-off between text-book ideal
28 | modeling and easy to use is difficult to judge and takes practice to develop.
29 |
30 | This talk will walk through some interesting cases of model design that I've
31 | encountered recently. I'll explain how I approached the problem and what we
32 | ended up with. These will include:
33 |
34 | * Modeling people who might simultaneously play different roles in the system.
35 | For example, a person who was a baseball player and then became a coach —
36 | each role has different attributes attached to it.
37 | * Modeling what appears to be a triangular dependency relationship with minimal
38 | redundancy in the data description and without needing really long query
39 | filters to access things.
40 | * Handling date ranges (or other measured data) of different degrees of
41 | accuracy and precision.
42 |
43 | This isn't a presentation on theoretical database design. Rather, concrete
44 | examples of creating such designs and guiding the decisions by what might work
45 | best in the final Django code. Hopefully, by listening to one person's approach
46 | (mine!), people faced with similar challenges will have another possible attack
47 | method in their toolbox.
48 |
49 | Setup
50 | ======
51 |
52 | Everything is configured to create an SQLite database and an automatic admin
53 | user. Simply run::
54 |
55 | python manage.py syncdb --noinput
56 |
57 | The admin user has username and password both set to *"admin"* (with the
58 | quotes).
59 |
60 | Tour of the code
61 | =================
62 |
63 | There are two applications included in this code package, providing models and
64 | a brief amount of supporting code for the two cases covered in the
65 | presentation.
66 |
67 | The `dates/` application is a pair of simple models and is the easier of the
68 | two cases. The `sports/` application is a tighter group of related models, that
69 | has been reduced (over the course of the presentation) to something manageable.
70 | The admin presentation for these models contains one enhancement: the team
71 | display page includes extra information about the current members and coaches
72 | (have a look in the templates directory to see how that is accomplished).
73 |
74 | By default, both applications will be installed with sample data and are
75 | viewable via Django's admin interface.
76 |
77 | Good luck!
78 |
79 | Malcolm Tredinnick
80 | (Sydney, Australia)
81 |
82 |
--------------------------------------------------------------------------------
/fixtures/sports.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "fields": {
4 | "name": "Fred Flintstone"
5 | },
6 | "model": "sports.person",
7 | "pk": 1
8 | },
9 | {
10 | "fields": {
11 | "name": "Barney Rubble"
12 | },
13 | "model": "sports.person",
14 | "pk": 2
15 | },
16 | {
17 | "fields": {
18 | "name": "Bam-Bam"
19 | },
20 | "model": "sports.person",
21 | "pk": 3
22 | },
23 | {
24 | "fields": {
25 | "name": "Pebbles"
26 | },
27 | "model": "sports.person",
28 | "pk": 4
29 | },
30 | {
31 | "fields": {
32 | "name": "Dino"
33 | },
34 | "model": "sports.person",
35 | "pk": 5
36 | },
37 | {
38 | "fields": {
39 | "name": "Bedrock Bumblebees"
40 | },
41 | "model": "sports.team",
42 | "pk": 1
43 | },
44 | {
45 | "fields": {
46 | "name": "Whackity-sacks"
47 | },
48 | "model": "sports.team",
49 | "pk": 2
50 | },
51 | {
52 | "fields": {
53 | "name": "Premier"
54 | },
55 | "model": "sports.league",
56 | "pk": 1
57 | },
58 | {
59 | "fields": {
60 | "name": "Old-timers"
61 | },
62 | "model": "sports.league",
63 | "pk": 2
64 | },
65 | {
66 | "fields": {
67 | "departed": "2002-06-30",
68 | "joined": "2000-04-12",
69 | "league": 2,
70 | "team": 2
71 | },
72 | "model": "sports.leagueteam",
73 | "pk": 1
74 | },
75 | {
76 | "fields": {
77 | "departed": null,
78 | "joined": "2002-07-01",
79 | "league": 1,
80 | "team": 2
81 | },
82 | "model": "sports.leagueteam",
83 | "pk": 2
84 | },
85 | {
86 | "fields": {
87 | "departed": null,
88 | "joined": "2002-07-01",
89 | "league": 1,
90 | "team": 1
91 | },
92 | "model": "sports.leagueteam",
93 | "pk": 3
94 | },
95 | {
96 | "fields": {
97 | "departed": "2002-06-30",
98 | "joined": "2000-04-12",
99 | "league": 2,
100 | "umpire": 5
101 | },
102 | "model": "sports.leagueumpire",
103 | "pk": 1
104 | },
105 | {
106 | "fields": {
107 | "departed": null,
108 | "joined": "2002-07-01",
109 | "league": 1,
110 | "umpire": 5
111 | },
112 | "model": "sports.leagueumpire",
113 | "pk": 2
114 | },
115 | {
116 | "fields": {
117 | "departed": null,
118 | "joined": "2002-07-01",
119 | "person": 1,
120 | "role": "P",
121 | "team": 2
122 | },
123 | "model": "sports.teammember",
124 | "pk": 1
125 | },
126 | {
127 | "fields": {
128 | "departed": null,
129 | "joined": "2002-07-01",
130 | "person": 2,
131 | "role": "P",
132 | "team": 2
133 | },
134 | "model": "sports.teammember",
135 | "pk": 2
136 | },
137 | {
138 | "fields": {
139 | "departed": null,
140 | "joined": "2002-07-01",
141 | "person": 4,
142 | "role": "C",
143 | "team": 2
144 | },
145 | "model": "sports.teammember",
146 | "pk": 3
147 | }
148 | ]
149 |
--------------------------------------------------------------------------------
/fixtures/initial_data.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "fields": {
4 | "date_joined": "2010-09-05 13:52:36",
5 | "email": "invalid@example.com",
6 | "first_name": "",
7 | "groups": [],
8 | "is_active": true,
9 | "is_staff": true,
10 | "is_superuser": true,
11 | "last_login": "2010-09-05 13:52:36",
12 | "last_name": "",
13 | "password": "sha1$c4e7f$383259f017f100f2ee8b4d233e4232501a896f36",
14 | "user_permissions": [],
15 | "username": "admin"
16 | },
17 | "model": "auth.user",
18 | "pk": 1
19 | },
20 | {
21 | "fields": {
22 | "name": "Fred Flintstone"
23 | },
24 | "model": "sports.person",
25 | "pk": 1
26 | },
27 | {
28 | "fields": {
29 | "name": "Barney Rubble"
30 | },
31 | "model": "sports.person",
32 | "pk": 2
33 | },
34 | {
35 | "fields": {
36 | "name": "Bam-Bam"
37 | },
38 | "model": "sports.person",
39 | "pk": 3
40 | },
41 | {
42 | "fields": {
43 | "name": "Pebbles"
44 | },
45 | "model": "sports.person",
46 | "pk": 4
47 | },
48 | {
49 | "fields": {
50 | "name": "Dino"
51 | },
52 | "model": "sports.person",
53 | "pk": 5
54 | },
55 | {
56 | "fields": {
57 | "name": "Bedrock Bumblebees"
58 | },
59 | "model": "sports.team",
60 | "pk": 1
61 | },
62 | {
63 | "fields": {
64 | "name": "Whackity-sacks"
65 | },
66 | "model": "sports.team",
67 | "pk": 2
68 | },
69 | {
70 | "fields": {
71 | "name": "Premier"
72 | },
73 | "model": "sports.league",
74 | "pk": 1
75 | },
76 | {
77 | "fields": {
78 | "name": "Old-timers"
79 | },
80 | "model": "sports.league",
81 | "pk": 2
82 | },
83 | {
84 | "fields": {
85 | "departed": "2002-06-30",
86 | "joined": "2000-04-12",
87 | "league": 2,
88 | "team": 2
89 | },
90 | "model": "sports.leagueteam",
91 | "pk": 1
92 | },
93 | {
94 | "fields": {
95 | "departed": null,
96 | "joined": "2002-07-01",
97 | "league": 1,
98 | "team": 2
99 | },
100 | "model": "sports.leagueteam",
101 | "pk": 2
102 | },
103 | {
104 | "fields": {
105 | "departed": null,
106 | "joined": "2002-07-01",
107 | "league": 1,
108 | "team": 1
109 | },
110 | "model": "sports.leagueteam",
111 | "pk": 3
112 | },
113 | {
114 | "fields": {
115 | "departed": "2002-06-30",
116 | "joined": "2000-04-12",
117 | "league": 2,
118 | "umpire": 5
119 | },
120 | "model": "sports.leagueumpire",
121 | "pk": 1
122 | },
123 | {
124 | "fields": {
125 | "departed": null,
126 | "joined": "2002-07-01",
127 | "league": 1,
128 | "umpire": 5
129 | },
130 | "model": "sports.leagueumpire",
131 | "pk": 2
132 | },
133 | {
134 | "fields": {
135 | "departed": null,
136 | "joined": "2002-07-01",
137 | "person": 1,
138 | "role": "P",
139 | "team": 2
140 | },
141 | "model": "sports.teammember",
142 | "pk": 1
143 | },
144 | {
145 | "fields": {
146 | "departed": null,
147 | "joined": "2002-07-01",
148 | "person": 2,
149 | "role": "P",
150 | "team": 2
151 | },
152 | "model": "sports.teammember",
153 | "pk": 2
154 | },
155 | {
156 | "fields": {
157 | "departed": null,
158 | "joined": "2002-07-01",
159 | "person": 4,
160 | "role": "C",
161 | "team": 2
162 | },
163 | "model": "sports.teammember",
164 | "pk": 3
165 | },
166 | {
167 | "fields": {
168 | "date": "1937-11-01",
169 | "precision": 0
170 | },
171 | "model": "dates.date",
172 | "pk": 1
173 | },
174 | {
175 | "fields": {
176 | "date": "1891-08-01",
177 | "precision": 1
178 | },
179 | "model": "dates.date",
180 | "pk": 2
181 | },
182 | {
183 | "fields": {
184 | "date": "1975-11-11",
185 | "precision": 2
186 | },
187 | "model": "dates.date",
188 | "pk": 3
189 | },
190 | {
191 | "fields": {
192 | "date": "1940-04-05",
193 | "precision": 3
194 | },
195 | "model": "dates.date",
196 | "pk": 4
197 | },
198 | {
199 | "fields": {
200 | "date": "1000-01-01",
201 | "precision": 4
202 | },
203 | "model": "dates.date",
204 | "pk": 5
205 | },
206 | {
207 | "fields": {
208 | "date": "0101-01-01",
209 | "precision": 4
210 | },
211 | "model": "dates.date",
212 | "pk": 6
213 | },
214 | {
215 | "fields": {
216 | "date": "0400-01-01",
217 | "precision": 4
218 | },
219 | "model": "dates.date",
220 | "pk": 7
221 | },
222 | {
223 | "fields": {
224 | "date": "1201-02-02",
225 | "precision": 4
226 | },
227 | "model": "dates.date",
228 | "pk": 8
229 | },
230 | {
231 | "fields": {
232 | "date": "1752-06-30",
233 | "precision": 3
234 | },
235 | "model": "dates.date",
236 | "pk": 9
237 | },
238 | {
239 | "fields": {
240 | "date": "1905-01-01",
241 | "precision": 3
242 | },
243 | "model": "dates.date",
244 | "pk": 10
245 | },
246 | {
247 | "fields": {
248 | "date": "2010-09-08",
249 | "precision": 0
250 | },
251 | "model": "dates.date",
252 | "pk": 11
253 | },
254 | {
255 | "fields": {
256 | "date": "1201-02-02",
257 | "precision": 4
258 | },
259 | "model": "dates.date",
260 | "pk": 12
261 | },
262 | {
263 | "fields": {
264 | "date": "1905-01-01",
265 | "precision": 3
266 | },
267 | "model": "dates.date",
268 | "pk": 13
269 | },
270 | {
271 | "fields": {
272 | "end": 7,
273 | "start": 6
274 | },
275 | "model": "dates.daterange",
276 | "pk": 1
277 | },
278 | {
279 | "fields": {
280 | "end": 9,
281 | "start": 8
282 | },
283 | "model": "dates.daterange",
284 | "pk": 2
285 | },
286 | {
287 | "fields": {
288 | "end": 11,
289 | "start": 10
290 | },
291 | "model": "dates.daterange",
292 | "pk": 3
293 | },
294 | {
295 | "fields": {
296 | "end": null,
297 | "start": 12
298 | },
299 | "model": "dates.daterange",
300 | "pk": 4
301 | },
302 | {
303 | "fields": {
304 | "end": null,
305 | "start": 13
306 | },
307 | "model": "dates.daterange",
308 | "pk": 5
309 | }
310 | ]
311 |
--------------------------------------------------------------------------------