├── __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 | 10 | 11 |

Current coaches

12 | 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 | --------------------------------------------------------------------------------