From 75f737ade80771eb117e2b7e1cc5968010556f66 Mon Sep 17 00:00:00 2001 From: Harry Marr Date: Sun, 22 Nov 2009 23:26:21 +0000 Subject: [PATCH] Added docs directory, started tutorial --- .gitignore | 1 + docs/.themes/nature/static/nature.css_t | 229 +++++++++++++++++++++ docs/.themes/nature/static/pygments.css | 54 +++++ docs/.themes/nature/theme.conf | 4 + docs/Makefile | 89 ++++++++ docs/code/tumblelog.py | 66 ++++++ docs/conf.py | 194 ++++++++++++++++++ docs/index.rst | 22 ++ docs/tutorial.rst | 259 ++++++++++++++++++++++++ 9 files changed, 918 insertions(+) create mode 100644 docs/.themes/nature/static/nature.css_t create mode 100644 docs/.themes/nature/static/pygments.css create mode 100644 docs/.themes/nature/theme.conf create mode 100644 docs/Makefile create mode 100644 docs/code/tumblelog.py create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/tutorial.rst diff --git a/.gitignore b/.gitignore index d18402de..e191be82 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.pyc .*.swp +docs/.build diff --git a/docs/.themes/nature/static/nature.css_t b/docs/.themes/nature/static/nature.css_t new file mode 100644 index 00000000..03b0379d --- /dev/null +++ b/docs/.themes/nature/static/nature.css_t @@ -0,0 +1,229 @@ +/** + * Sphinx stylesheet -- default theme + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Arial, sans-serif; + font-size: 100%; + background-color: #111; + color: #555; + margin: 0; + padding: 0; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 230px; +} + +hr{ + border: 1px solid #B1B4B6; +} + +div.document { + background-color: #eee; +} + +div.body { + background-color: #ffffff; + color: #3E4349; + padding: 0 30px 30px 30px; + font-size: 0.8em; +} + +div.footer { + color: #555; + width: 100%; + padding: 13px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #444; + text-decoration: underline; +} + +div.related { + background-color: #6BA81E; + line-height: 32px; + color: #fff; + text-shadow: 0px 1px 0 #444; + font-size: 0.80em; +} + +div.related a { + color: #E2F3CC; +} + +div.sphinxsidebar { + font-size: 0.75em; + line-height: 1.5em; +} + +div.sphinxsidebarwrapper{ + padding: 20px 0; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Arial, sans-serif; + color: #222; + font-size: 1.2em; + font-weight: normal; + margin: 0; + padding: 5px 10px; + background-color: #ddd; + text-shadow: 1px 1px 0 white +} + +div.sphinxsidebar h4{ + font-size: 1.1em; +} + +div.sphinxsidebar h3 a { + color: #444; +} + + +div.sphinxsidebar p { + color: #888; + padding: 5px 20px; +} + +div.sphinxsidebar p.topless { +} + +div.sphinxsidebar ul { + margin: 10px 20px; + padding: 0; + color: #000; +} + +div.sphinxsidebar a { + color: #444; +} + +div.sphinxsidebar input { + border: 1px solid #ccc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar input[type=text]{ + margin-left: 20px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #005B81; + text-decoration: none; +} + +a:hover { + color: #E32E00; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: Arial, sans-serif; + background-color: #BED4EB; + font-weight: normal; + color: #212224; + margin: 30px 0px 10px 0px; + padding: 5px 0 5px 10px; + text-shadow: 0px 1px 0 white +} + +div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } +div.body h2 { font-size: 150%; background-color: #C8D5E3; } +div.body h3 { font-size: 120%; background-color: #D8DEE3; } +div.body h4 { font-size: 110%; background-color: #D8DEE3; } +div.body h5 { font-size: 100%; background-color: #D8DEE3; } +div.body h6 { font-size: 100%; background-color: #D8DEE3; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li { + line-height: 1.5em; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.highlight{ + background-color: white; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 10px; + background-color: White; + color: #222; + line-height: 1.2em; + border: 1px solid #C6C9CB; + font-size: 1.2em; + margin: 1.5em 0 1.5em 0; + -webkit-box-shadow: 1px 1px 1px #d8d8d8; + -moz-box-shadow: 1px 1px 1px #d8d8d8; +} + +tt { + background-color: #ecf0f3; + color: #222; + padding: 1px 2px; + font-size: 1.2em; + font-family: monospace; +} diff --git a/docs/.themes/nature/static/pygments.css b/docs/.themes/nature/static/pygments.css new file mode 100644 index 00000000..652b7612 --- /dev/null +++ b/docs/.themes/nature/static/pygments.css @@ -0,0 +1,54 @@ +.c { color: #999988; font-style: italic } /* Comment */ +.k { font-weight: bold } /* Keyword */ +.o { font-weight: bold } /* Operator */ +.cm { color: #999988; font-style: italic } /* Comment.Multiline */ +.cp { color: #999999; font-weight: bold } /* Comment.preproc */ +.c1 { color: #999988; font-style: italic } /* Comment.Single */ +.gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ +.ge { font-style: italic } /* Generic.Emph */ +.gr { color: #aa0000 } /* Generic.Error */ +.gh { color: #999999 } /* Generic.Heading */ +.gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ +.go { color: #111 } /* Generic.Output */ +.gp { color: #555555 } /* Generic.Prompt */ +.gs { font-weight: bold } /* Generic.Strong */ +.gu { color: #aaaaaa } /* Generic.Subheading */ +.gt { color: #aa0000 } /* Generic.Traceback */ +.kc { font-weight: bold } /* Keyword.Constant */ +.kd { font-weight: bold } /* Keyword.Declaration */ +.kp { font-weight: bold } /* Keyword.Pseudo */ +.kr { font-weight: bold } /* Keyword.Reserved */ +.kt { color: #445588; font-weight: bold } /* Keyword.Type */ +.m { color: #009999 } /* Literal.Number */ +.s { color: #bb8844 } /* Literal.String */ +.na { color: #008080 } /* Name.Attribute */ +.nb { color: #999999 } /* Name.Builtin */ +.nc { color: #445588; font-weight: bold } /* Name.Class */ +.no { color: #ff99ff } /* Name.Constant */ +.ni { color: #800080 } /* Name.Entity */ +.ne { color: #990000; font-weight: bold } /* Name.Exception */ +.nf { color: #990000; font-weight: bold } /* Name.Function */ +.nn { color: #555555 } /* Name.Namespace */ +.nt { color: #000080 } /* Name.Tag */ +.nv { color: purple } /* Name.Variable */ +.ow { font-weight: bold } /* Operator.Word */ +.mf { color: #009999 } /* Literal.Number.Float */ +.mh { color: #009999 } /* Literal.Number.Hex */ +.mi { color: #009999 } /* Literal.Number.Integer */ +.mo { color: #009999 } /* Literal.Number.Oct */ +.sb { color: #bb8844 } /* Literal.String.Backtick */ +.sc { color: #bb8844 } /* Literal.String.Char */ +.sd { color: #bb8844 } /* Literal.String.Doc */ +.s2 { color: #bb8844 } /* Literal.String.Double */ +.se { color: #bb8844 } /* Literal.String.Escape */ +.sh { color: #bb8844 } /* Literal.String.Heredoc */ +.si { color: #bb8844 } /* Literal.String.Interpol */ +.sx { color: #bb8844 } /* Literal.String.Other */ +.sr { color: #808000 } /* Literal.String.Regex */ +.s1 { color: #bb8844 } /* Literal.String.Single */ +.ss { color: #bb8844 } /* Literal.String.Symbol */ +.bp { color: #999999 } /* Name.Builtin.Pseudo */ +.vc { color: #ff99ff } /* Name.Variable.Class */ +.vg { color: #ff99ff } /* Name.Variable.Global */ +.vi { color: #ff99ff } /* Name.Variable.Instance */ +.il { color: #009999 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/.themes/nature/theme.conf b/docs/.themes/nature/theme.conf new file mode 100644 index 00000000..1cc40044 --- /dev/null +++ b/docs/.themes/nature/theme.conf @@ -0,0 +1,4 @@ +[theme] +inherit = basic +stylesheet = nature.css +pygments_style = tango diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..66cd648f --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,89 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = .build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/MongoEngine.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/MongoEngine.qhc" + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/code/tumblelog.py b/docs/code/tumblelog.py new file mode 100644 index 00000000..6ba1eee2 --- /dev/null +++ b/docs/code/tumblelog.py @@ -0,0 +1,66 @@ +from mongoengine import * + +connect('tumblelog') + +class Comment(EmbeddedDocument): + content = StringField() + name = StringField(max_length=120) + +class User(Document): + email = StringField(required=True) + first_name = StringField(max_length=50) + last_name = StringField(max_length=50) + +class Post(Document): + title = StringField(max_length=120, required=True) + author = ReferenceField(User) + tags = ListField(StringField(max_length=30)) + comments = ListField(EmbeddedDocumentField(Comment)) + +class TextPost(Post): + content = StringField() + +class ImagePost(Post): + image_path = StringField() + +class LinkPost(Post): + link_url = StringField() + +Post.drop_collection() + +john = User(email='jdoe@example.com', first_name='John', last_name='Doe') +john.save() + +post1 = TextPost(title='Fun with MongoEngine', author=john) +post1.content = 'Took a look at MongoEngine today, looks pretty cool.' +post1.tags = ['mongodb', 'mongoengine'] +post1.save() + +post2 = LinkPost(title='MongoEngine Documentation', author=john) +post2.link_url = 'http://tractiondigital.com/labs/mongoengine/docs' +post2.tags = ['mongoengine'] +post2.save() + +print 'ALL POSTS' +print +for post in Post.objects: + print post.title + print '=' * len(post.title) + + if isinstance(post, TextPost): + print post.content + + if isinstance(post, LinkPost): + print 'Link:', post.link_url + + print +print + +print 'POSTS TAGGED \'MONGODB\'' +print +for post in Post.objects(tags='mongodb'): + print post.title +print + +num_posts = Post.objects(tags='mongodb').count() +print 'Found %d posts with tag "mongodb"' % num_posts diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..5b29b371 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +# +# MongoEngine documentation build configuration file, created by +# sphinx-quickstart on Sun Nov 22 18:14:13 2009. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.append(os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['.templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'MongoEngine' +copyright = u'2009, Harry Marr' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = [] + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = ['.build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +html_theme = 'nature' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +html_theme_path = ['.themes'] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['.static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'MongoEnginedoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +latex_paper_size = 'a4' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'MongoEngine.tex', u'MongoEngine Documentation', + u'Harry Marr', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..5fab07a6 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +.. MongoEngine documentation master file, created by + sphinx-quickstart on Sun Nov 22 18:14:13 2009. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to MongoEngine's documentation! +======================================= + +Contents: + +.. toctree:: + :maxdepth: 2 + + tutorial.rst + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/tutorial.rst b/docs/tutorial.rst new file mode 100644 index 00000000..9e42b28b --- /dev/null +++ b/docs/tutorial.rst @@ -0,0 +1,259 @@ +Tutorial +======== +This tutorial introduces **MongoEngine** by means of example --- we will walk +through how to create a simple **Tumblelog** application. A Tumblelog is a type +of blog where posts are not constrained to being conventional text-based posts. +As well as text-based entries, users may post images, links, videos, etc. For +simplicity's sake, we'll stick to text, image and link entries in our +application. As the purpose of this tutorial is to introduce MongoEngine, we'll +focus on the data-modelling side of the application, leaving out a user +interface. + +Connecting to MongoDB +--------------------- +Before we start, you should make sure that you have a copy of MongoDB running +in an accessible location --- running it locally will be easier, but if that is +not an option then it may be run on a remote server. + +Before we can start using MongoEngine, we need to tell it how to connect to our +instance of **mongod**. For this we use the :func:`mongoengine.connect` +function. The only argument we need to provide is the name of the MongoDB +database to use:: + + from mongoengine import * + + connect('tumblelog') + +This will connect to a mongod instance running locally on the default port. To +connect to a mongod instance running elsewhere, we may specify the host and +port explicitly:: + + connect('tumblelog', host='192.168.1.35', port=12345) + +Defining our documents +---------------------- +MongoDB is *schemaless*, which means that no schema is enforced by the database +--- we may add and remove fields however we want and MongoDB won't complain. +This makes life a lot easier in many regards, especially when it comes to +migrations. However, defining schemata for our documents can help to iron out +bugs involving incorrect types or missing fields, and also allow us to define +utility methods on our documents in the same way that traditional :abbr:`ORMs +(Object-Relational Mappers)` do. + +In our Tumblelog application we need to store several different types of +information. We will need to have a collection of **users**, so that we may +link posts to an individual. We also need to store our different types +**posts** (text, image and link) in the database. For to aid navigation of our +Tumblelog, posts may have **tags** associated with them, so that the list of +posts shown to the user may be limited to posts that have a specified tag. +Finally, it would be nice if **comments** could be added to posts. We'll start +with **users**, as the others are slightly more involved. + +Users +^^^^^ +Just as if we were using a relational database with an ORM, we need to define +which fields a :class:`User` may have, and what their types will be:: + + class User(Document): + email = StringField(required=True) + first_name = StringField(max_length=50) + last_name = StringField(max_length=50) + +This looks similar to how a the structure of a table would be defined in a +regular ORM. The key difference is that this schema will never be passed on to +MongoDB --- this will only be enforced at the application level. Also, the User +documents will be stored in a MongoDB *collection* rather than a table. + +Posts, Comments and Tags +^^^^^^^^^^^^^^^^^^^^^^^^ +Well that wasn't too bad, was it? Now we'll think about how to store the rest +of the information. If we were using a relational database, we would most +likely have a table of **posts**, a table of **comments** and a table of +**tags**. To associate the comments with individual posts, we would put a +column in the comments table that contained a foreign key to the posts table. +We'd also need a link table to provide the many-to-many relationship between +posts and tags. Then we'd need to address the problem of storing the +specialised post-types (text, image and link). There are several ways we can +achieve this, but each of them have their problems --- none of them stand out +as particularly intuitive solutions. + +Posts +""""" +But MongoDB *isn't* a relational database, so we're not going to do it that +way. As it turns out, we can use MongoDB's schemaless nature to provide us with +a much nicer solution. We will store all of the posts in *one collection* --- +each post type will just have the fields it needs. If we later want to add +video posts, we don't have to modify the collection at all, we just *start +using* the new fields we need to support video posts. This fits with the +Object-Oriented principle of *inheritance* nicely. We can think of +:class:`Post` as an base class, and :class:`TextPost`, :class:`ImagePost` and +:class:`LinkPost` as subclasses of :class:`Post`. In fact, MongoEngine supports +this kind of modelling out of the box:: + + class Post(Document): + title = StringField(max_length=120, required=True) + author = ReferenceField(User) + + class TextPost(Post): + content = StringField() + + class ImagePost(Post): + image_path = StringField() + + class LinkPost(Post): + link_url = StringField() + +We are storing a reference to the author of the posts using a +:class:`mongoengine.ReferenceField` object. These are similar to foreign key +fields in traditional ORMs, and are automatically translated into references +when they are saved, and dereferenced when they are loaded. + +Tags +"""" +Now that we have our Post models figured out, how will we attach tags to them? +MongoDB allows us to store lists of items natively, so rather than having a +link table, we can just store a list of tags in each post. Also, for both +efficiency and simplicity's sake, we'll store the tags as strings directly +within the post, rather than storing references to tags in a separate +collection. Especially as tags are generally very short (often even shorted +than a document's id), this denormalisation won't impact very strongly on the +size of our database. So let's take a look that the code our modified +:class:`Post` class:: + + class Post(Document): + title = StringField(max_length=120, required=True) + author = ReferenceField(User) + tags = ListField(StringField(max_length=30)) + +The :class:`mongoengine.ListField` object that is used to define a Post's tags +takes a field object as its first argument --- this means that you can have +lists of any type of field (including lists). Note that we don't need to +modify the specialised post types as they all inherit from :class:`Post`. + +Comments +"""""""" +A comment is typically associated with *one* post. In a relational database, to +display a post with its comments, we would have to retrieve the post from the +database, then query the database again for the comments associated with the +post. This works, but there is no real reason to be storing the comments +separately from their associated posts, other than to work around the +relational model. Using MongoDB we can store the comments as a list of +*embedded documents* directly on the post document. An embedded document should +be treated no differently that a regular document; it just doesn't have its own +collection. Using MongoEngine, we can define the structure of embedded +documents, along with utility methods, in exactly the same way we do with +regular documents:: + + class Comment(EmbeddedDocument): + content = StringField() + name = StringField(max_length=120) + +We can then store a list of comment documents in our post document:: + + class Post(Document): + title = StringField(max_length=120, required=True) + author = ReferenceField(User) + tags = ListField(StringField(max_length=30)) + comments = ListField(EmbeddedDocumentField(Comment)) + +Adding data to our Tumblelog +---------------------------- +Now that we've defined how our documents will be structured, lets start adding +some documents to the database. Firstly, we'll need to create a :class:`User` +object:: + + john = User(email='jdoe@example.com', first_name='John', last_name='Doe') + john.save() + +Simple, eh? Note that only fields with ``required=True`` need to be specified +in the constructor, we could have also defined our user using attribute +syntax:: + + john = User(email='jdoe@example.com') + john.first_name = 'John' + john.last_name = 'Doe' + john.save() + +Now that we've got our user in the database, lets add a couple of posts:: + + post1 = TextPost(title='Fun with MongoEngine', author=john) + post1.content = 'Took a look at MongoEngine today, looks pretty cool.' + post1.tags = ['mongodb', 'mongoengine'] + post1.save() + + post2 = LinkPost(title='MongoEngine Documentation', author=john) + post2.link_url = 'http://tractiondigital.com/labs/mongoengine/docs' + post2.tags = ['mongoengine'] + post2.save() + +Note that if you change a field on a object that has already been saved, then +call :meth:`save` again, the document will be updated. + +Accessing our data +------------------ +So now we've got a couple of posts in our database, how do we display them? +Each document class (i.e. any class that inherits either directly or indirectly +from :class:`mongoengine.Document`) has an :attr:`objects` attribute, which is +used to access the documents in the database associated with that class. So +lets see how we can get our posts' titles:: + + for post in Post.objects: + print post.title + +Retrieving type-specific information +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +This will print the titles of our posts, one on each line. But What if we want +to access the type-specific data (link_url, content, etc.)? One way is simply +to use the :attr:`objects` attribute of a subclass of :class:`Post`:: + + for post in TextPost.objects: + print post.content + +Using TextPost's :attr:`objects` attribute only returns documents that were +created using :class:`TextPost`. Actually, there is a more general rule here: +the :attr:`objects` attribute of any subclass of :class:`mongoengine.Document` +only looks for documents that were created using that subclass or one of its +subclasses. + +So how would we display all of our posts, showing only the information that +corresponds to each post's specific type? As you might have guessed, there is a +better way than just using each of the subclasses individually. When we used +:class:`Post`'s :attr:`objects` attribute earlier, the objects being returned +weren't actually instances of :class:`Post` --- they were instances of the +subclass of :class:`Post` that matches the post's type. Lets look at how this +works in practice:: + + for post in Post.objects: + print post.title + print '=' * len(post.title) + + if isinstance(post, TextPost): + print post.content + + if isinstance(post, LinkPost): + print 'Link:', post.link_url + + print + +This would print the title of each post, followed by the content if it was a +text post, and "Link: " if it was a link post. + +Searching our posts by tag +^^^^^^^^^^^^^^^^^^^^^^^^^^ +The :attr:`objects` attribute of a :class:`mongoengine.Document` is actually a +:class:`mongoengine.QuerySet` object. This lazily queries the database only +when you need the data. It may also be filtered to narrow down your query. Lets +adjust our query so that only posts with the tag "mongodb" are returned:: + + for post in Post.objects(tags='mongodb'): + print post.title + +There are also methods available on :class:`mongoengine.QuerySet` objects that +allow different results to be returned, for example, calling :meth:`first` on +the :attr:`objects` attribute will return a single document, the first matched +by the query you provide. Aggregation functions may also be used on +:class:`mongoengine.QuerySet` objects:: + + num_posts = Post.objects(tags='mongodb').count() + print 'Found % posts with tag "mongodb"' % num_posts +