├── .gitignore ├── LICENCE ├── README.rst ├── __init__.py ├── doc ├── Makefile ├── conf.py ├── contents.rst ├── index.rst ├── make.bat ├── pdfindex.rst └── topics │ ├── admin-migration.rst │ ├── database-migration.rst │ ├── index.rst │ ├── model-migration.rst │ └── model.rst ├── epio.ini ├── manage.py ├── requirements.txt ├── responses ├── __init__.py └── models.py ├── settings ├── __init__.py └── default.py ├── surveymaker ├── __init__.py ├── admin.py ├── dynamic_models.py ├── fields.py ├── fixtures │ └── initial_data.json ├── models.py ├── signals.py ├── static │ └── css │ │ └── style.css ├── templates │ ├── base.html │ └── surveymaker │ │ ├── all.html │ │ └── survey_form.html ├── utils.py └── views.py └── urls.py /.gitignore: -------------------------------------------------------------------------------- 1 | test.db 2 | *.pyc 3 | env 4 | settings/epio.py 5 | .epio-app 6 | TODO 7 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Will Hardy 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | The names of this software's contributors may not be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Runtime dynamic models with Django 2 | ================================== 3 | 4 | This is an example project to demonstrate a number of techniques that allow 5 | dynamic models to work. 6 | This was written to accompany a talk at 2011 Djangocon.eu, the text of the 7 | talk is provided in `this project's documentation `_ and a video 8 | of the presentation `can be found here `_. 9 | 10 | The project is a simple survey maker, where admin users can define surveys. 11 | The responses can then be stored in a customised table for that survey, 12 | made possible with a dynamic model for each survey. Tables are migrated 13 | when relevant changes are made, using a shared cache to keep multiple 14 | processes in sync. 15 | 16 | This was written reasonably quickly, but effort has been made to keep it simple. 17 | There will no doubt be typos and bugs, maybe even some conceptual problems. 18 | Please provide any feedback you might have and I will be happy to improve this 19 | implementation. The aim of this project is to demonstrate that dynamic models 20 | are possible and can be made to work reliably. 21 | 22 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willhardy/dynamic-models/12327589973ea1492a4b54922c90a0b479188e20/__init__.py -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/RuntimeDynamicModels.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/RuntimeDynamicModels.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/RuntimeDynamicModels" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/RuntimeDynamicModels" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Runtime Dynamic Models documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Jun 8 01:55:36 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Runtime Dynamic Models' 44 | copyright = u'2011, Will Hardy' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '1.0' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '1.0' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'RuntimeDynamicModelsdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('pdfindex', 'RuntimeDynamicModels.tex', u'Runtime Dynamic Models Documentation', 182 | u'Will Hardy', 'howto'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # If true, show page references after internal links. 194 | #latex_show_pagerefs = False 195 | 196 | # If true, show URL addresses after external links. 197 | #latex_show_urls = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | #latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | #latex_domain_indices = True 207 | 208 | 209 | # -- Options for manual page output -------------------------------------------- 210 | 211 | # One entry per manual page. List of tuples 212 | # (source start file, name, description, authors, manual section). 213 | man_pages = [ 214 | ('index', 'runtimedynamicmodels', u'Runtime Dynamic Models Documentation', 215 | [u'Will Hardy'], 1) 216 | ] 217 | 218 | 219 | # -- Options for Epub output --------------------------------------------------- 220 | 221 | # Bibliographic Dublin Core info. 222 | epub_title = u'Runtime Dynamic Models' 223 | epub_author = u'Will Hardy' 224 | epub_publisher = u'Will Hardy' 225 | epub_copyright = u'2011, Will Hardy' 226 | 227 | # The language of the text. It defaults to the language option 228 | # or en if the language is not set. 229 | #epub_language = '' 230 | 231 | # The scheme of the identifier. Typical schemes are ISBN or URL. 232 | #epub_scheme = '' 233 | 234 | # The unique identifier of the text. This can be a ISBN number 235 | # or the project homepage. 236 | #epub_identifier = '' 237 | 238 | # A unique identification for the text. 239 | #epub_uid = '' 240 | 241 | # HTML files that should be inserted before the pages created by sphinx. 242 | # The format is a list of tuples containing the path and title. 243 | #epub_pre_files = [] 244 | 245 | # HTML files shat should be inserted after the pages created by sphinx. 246 | # The format is a list of tuples containing the path and title. 247 | #epub_post_files = [] 248 | 249 | # A list of files that should not be packed into the epub file. 250 | #epub_exclude_files = [] 251 | 252 | # The depth of the table of contents in toc.ncx. 253 | #epub_tocdepth = 3 254 | 255 | # Allow duplicate toc entries. 256 | #epub_tocdup = True 257 | -------------------------------------------------------------------------------- /doc/contents.rst: -------------------------------------------------------------------------------- 1 | .. _contents: 2 | 3 | ======================== 4 | Dynamic models: contents 5 | ======================== 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | topics/index 11 | 12 | 13 | Indicies, glossary and tables 14 | ----------------------------- 15 | 16 | * :ref:`genindex` 17 | * :ref:`modindex` 18 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. Main landing page for dynamic models documentation. 2 | TODO: 3 | - Add an overview, with practical examples 4 | - Discuss reasons to use / not to use this technique 5 | - Add small section on database errors to db migration page. 6 | - walkthrough the example implementation 7 | 8 | ================================== 9 | Runtime Dynamic Models with Django 10 | ================================== 11 | 12 | The flexibility of Python and Django allow developers to dynamically create 13 | models to store and access data using Django's ORM. 14 | But you need to be careful if you go down this road, especially if your models 15 | are set to change at runtime. 16 | This documentation will cover a number of things to consider when making use 17 | of runtime dynamic models. 18 | 19 | - :ref:`Defining a dynamic model factory ` 20 | - :ref:`Model migration ` 21 | - :ref:`Database schema migration ` 22 | - :ref:`Admin migration ` 23 | 24 | 25 | Example implementation 26 | ====================== 27 | 28 | An example implementation of dynamic models is also provided for reference. 29 | It is hosted here on GitHub: 30 | `willhardy/dynamic-models `_. 31 | 32 | DjangoCon.eu Talk 33 | ================= 34 | 35 | This topic was presented at DjangoCon and these notes have been written 36 | as supplementary documentation for that talk. 37 | The talk can be `viewed online here `_. 38 | 39 | 40 | Indices and tables 41 | ================== 42 | 43 | * :ref:`contents` 44 | * :ref:`genindex` 45 | * :ref:`modindex` 46 | * :ref:`search` 47 | 48 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\RuntimeDynamicModels.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\RuntimeDynamicModels.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /doc/pdfindex.rst: -------------------------------------------------------------------------------- 1 | .. PDF documentation 2 | 3 | ================================== 4 | Runtime Dynamic Models with Django 5 | ================================== 6 | 7 | The flexibility of Python and Django allow developers to dynamically create 8 | models to store and access data using Django's ORM. 9 | But you need to be careful if you go down this road, especially if your models 10 | are set to change at runtime. 11 | This documentation will cover a number of things to consider when making use 12 | of runtime dynamic models. 13 | 14 | An example implementation of dynamic models is also provided for reference. 15 | It is hosted here on GitHub: 16 | `willhardy/dynamic-models `_. 17 | 18 | This topic was presented at DjangoCon and these notes have been written 19 | as supplementary documentation for that talk. 20 | The talk can be `viewed online here `_. 21 | 22 | 23 | .. include:: /topics/model.rst 24 | .. include:: /topics/model-migration.rst 25 | .. include:: /topics/database-migration.rst 26 | .. include:: /topics/admin-migration.rst 27 | -------------------------------------------------------------------------------- /doc/topics/admin-migration.rst: -------------------------------------------------------------------------------- 1 | .. _topics-admin-migration: 2 | 3 | =============== 4 | Admin migration 5 | =============== 6 | 7 | When using Django's admin, you will need to update the admin site 8 | when your dynamic model changes. 9 | This is one area where it is not entirely straightforward, 10 | because we are now dealing with a "third-party app" that does not 11 | expect the given models to change. 12 | That said, there are ways around almost all of the main problems. 13 | 14 | Unregistering 15 | ------------- 16 | 17 | Calling ``site.unregister`` will not suffice, 18 | the model will have changed and the admin will not find the old dynamic model 19 | when given the new one. 20 | The solution is however straightforward, 21 | we search manually using the name of the old dynamic model, 22 | which will often be the same as the new model's name. 23 | 24 | The following code ensures the model is unregistered: 25 | 26 | .. code-block:: python 27 | 28 | from django.contrib import admin 29 | site = admin.site 30 | model = my_dynamic_model_factory() 31 | 32 | for reg_model in site._registry.keys(): 33 | if model._meta.db_table == reg_model._meta.db_table: 34 | del site._registry[reg_model] 35 | 36 | # Try the regular approach too, but this may be overdoing it 37 | try: 38 | site.unregister(model) 39 | except NotRegistered: 40 | pass 41 | 42 | .. note:: 43 | 44 | Again, this is accessing undocumented parts of Django's core. 45 | Please write a unit test to confirm that it works, 46 | so that you will notice any backwards incompatible changes in a 47 | future Django update. 48 | 49 | 50 | Clearing the URL cache 51 | ---------------------- 52 | 53 | Even though you may have successfully re-registered a newly changed model, 54 | Django caches URLs as soon as they are first loaded. 55 | The following idiomatic code will reset the URL cache, 56 | allowing new URLs to take effect. 57 | 58 | .. code-block:: python 59 | 60 | from django.conf import settings 61 | from django.utils.importlib import import_module 62 | from django.core.urlresolvers import clear_url_caches 63 | 64 | reload(import_module(settings.ROOT_URLCONF)) 65 | clear_url_caches() 66 | 67 | 68 | Timing Admin updates 69 | -------------------- 70 | 71 | Once again, Django's signals can be used to trigger an update of the Admin. 72 | Unfortunately, this cannot be done when the change takes place in another 73 | process. 74 | 75 | Because Django's admin doesn't use your factory function to access the model 76 | class (it uses the cached version), it cannot check the hash for validity 77 | nor can it rebuild when necessary. 78 | 79 | This isn't a problem if your Admin site is carefully place on a single server 80 | and all meaningful changes take place on the same instance. 81 | In reality, it's not always on a single server and background processes 82 | and tasks need to be able to alter the schema. 83 | 84 | A fix may involve pushing the update to the admin process, 85 | be it a rudimentary hook URL, or something much more sophisticated. 86 | 87 | -------------------------------------------------------------------------------- /doc/topics/database-migration.rst: -------------------------------------------------------------------------------- 1 | .. _topics-database-migration: 2 | 3 | ========================= 4 | Database schema migration 5 | ========================= 6 | 7 | As mentioned earlier, creating model classes at runtime means that the relevant 8 | database tables will not be created by Django when running syncdb; you will have 9 | to create them yourself. Additionally, if your dynamic models are likely to change 10 | you are going to have to handle database schema (and data) migration. 11 | 12 | 13 | Schema and data migrations with South 14 | ------------------------------------- 15 | 16 | Thankfully, `South`_ has a reliable set of functions 17 | to handle schema and database migrations for Django projects. 18 | When used in development, South can suggest migrations but does not attempt to 19 | automatically apply them without interaction from the developer. 20 | This can be different for your system, 21 | if you are able to recognised the required migration actions with 100% 22 | confidence, there should be no issue with automatically running schema and data 23 | migrations. 24 | That said, any automatic action is a dangerous one, be sure to err on the side 25 | of caution and avoid destructive operations as much as possible. 26 | 27 | .. _South: 28 | 29 | Here is a (perfectly safe) way to create a table from your dynamic model. 30 | 31 | .. code-block:: python 32 | 33 | from south.db import db 34 | 35 | model_class = generate_my_model_class() 36 | fields = [(f.name, f) for f in model_class._meta.local_fields] 37 | table_name = model_class._meta.db_table 38 | 39 | db.create_table(table_name, fields) 40 | 41 | # some fields (eg GeoDjango) require additional SQL to be executed 42 | db.execute_deferred_sql() 43 | 44 | Basic schema migration can also be easily performed. 45 | Note that if the column type changes in a way that requires data conversion, 46 | you may have to migrate the data manually. 47 | Remember to run ``execute_deferred_sql`` after adding a new table or column, 48 | to handle a number of special model fields (eg ``ForeignKey``, ``ManyToManyField``, 49 | GeoDjango fields etc). 50 | 51 | .. code-block:: python 52 | 53 | db.add_column(table_name, name, field) 54 | db.execute_deferred_sql() 55 | 56 | db.rename_column(table_name, old, new) 57 | db.rename_table(old_table_name, new_table_name) 58 | 59 | db.alter_column(table_name, name, field) 60 | 61 | Indexes and unique constraints may need to be handled separately: 62 | 63 | .. code-block:: python 64 | 65 | db.create_unique(table_name, columns) 66 | db.delete_unique(table_name, columns) 67 | db.create_index(table_name, column_names, unique=False) 68 | db.delete_index(table_name, column_name) 69 | 70 | db.create_primary_key(table_name, columns) # err... does your schema 71 | db.delete_primary_key(table_name) # really need to be so dynamic? 72 | 73 | 74 | If you really need to delete tables and columns, you can do that too. 75 | It's a good idea to avoid destructive operations until they're necessary. 76 | Leaving orphaned tables and columns for a period of time and cleaning 77 | them at a later date is perfectly acceptable. You may want to have your 78 | own deletion policy and process, depending on your needs. 79 | 80 | .. code-block:: python 81 | 82 | db.delete_table(table_name) 83 | db.delete_column(table_name, field) 84 | 85 | .. note:: 86 | Note that this South functionality is in the process of being merged into 87 | Django core. It will hopefully land in trunk in the near future. 88 | 89 | Timing the changes 90 | ------------------ 91 | 92 | Using Django's standard signals, you can perform the relevant actions to 93 | migrate the database schema at the right time. 94 | For example, create the new table on ``post_save`` when ``created=True``. 95 | 96 | You may also wish to run some conditional migrations at startup. 97 | For that you'll need to use the ``class_prepared`` signal, but wait until 98 | the models that your factory function require have all been prepared. 99 | The following function handles this timing. 100 | Place it in your ``models.py`` before any of the required models have 101 | been defined and it will call the given function when the time is right: 102 | 103 | .. code-block:: python 104 | 105 | when_classes_prepared(app_label, req_models, builder_fn) 106 | 107 | The function's implementation can be found in the example code, 108 | in ``surveymaker.utils``. 109 | 110 | Another useful feature is to be able to identify when a column rename is 111 | required. 112 | If your dynamic models are defined by Django models, it may be as simple 113 | as determining if an attribute on a model instance has been changed. 114 | You can do this with a combination of ``pre_save`` and ``post_save`` signals 115 | (see ``surveymaker.signals`` in example code for an example of this) 116 | or you can override the `__init__` method of the relevant model to store 117 | the original values when an instance is created. 118 | The ``post_save`` signal can then detect if a change was made and trigger the 119 | column rename. 120 | 121 | If you're concerned about failed migrations causing an inconsistent system 122 | state you may want to ensure that the migrations are in the same transaction 123 | as the changes that cause them. 124 | 125 | 126 | Introspection 127 | ------------- 128 | 129 | It may be useful to perform introspection, especially if you leave "deleted" 130 | tables and columns lying around, or if naming conflicts are possible 131 | (but please try to make them impossible). 132 | This means, the system will react in the way you want it to, 133 | for example by renaming or deleting the existing tables or by aborting the 134 | proposed schema migration. 135 | 136 | Django provides an interface for its supported databases, where existing 137 | table names and descriptions can be easily discovered: 138 | 139 | .. code-block:: python 140 | 141 | from django.db.connection import introspection 142 | from django.db import connection 143 | 144 | name = introspection.table_name_converter(table_name) 145 | 146 | # Is my table already there? 147 | print name in introspection.table_names() 148 | 149 | description = introspection.get_table_description(connection.cursor(), name) 150 | db_column_names = [row[0] for row in description] 151 | 152 | # Is my field's column already there? 153 | print myfield.column in db_column_names 154 | 155 | Note that this is limited to standard field types, some fields aren't exactly columns. 156 | 157 | -------------------------------------------------------------------------------- /doc/topics/index.rst: -------------------------------------------------------------------------------- 1 | .. _topics-index: 2 | 3 | Topics for Dynamic models 4 | ========================= 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | model 10 | database-migration 11 | model-migration 12 | admin-migration 13 | 14 | -------------------------------------------------------------------------------- /doc/topics/model-migration.rst: -------------------------------------------------------------------------------- 1 | .. _topics-model-migration: 2 | 3 | =============== 4 | Model migration 5 | =============== 6 | 7 | Model migration is simplest when you just regenerate the model using the 8 | usual factory function. 9 | The problem is when the change has been instigated in another process, 10 | the current process will not know that the model class needs to be 11 | regenerated again. 12 | Because it would generally be insane to regenerate the model on every view, 13 | you will need to send a message to other processes, to inform them that 14 | their cached model class is no longer valid. 15 | 16 | The most efficient way to check equality is to make a hash describing 17 | the dynamic model, 18 | and let processes compare their hash with the latest version. 19 | 20 | Generating a hash 21 | ----------------- 22 | 23 | You're free to do it however you like, just make sure it is deterministic, 24 | ie you explicitly order the encoded fields. 25 | 26 | In the provided example, a json representation of the fields relevant to 27 | dynamic model is used to create a hash. For example: 28 | 29 | .. code-block:: python 30 | 31 | from django.utils import simplejson 32 | from django.utils.hashcompat import md5_constructor 33 | 34 | i = my_instance_that_defines_a_dynamic_model 35 | val = (i.slug, i.name, [i.fields for i in self.field_set.all()) 36 | print md5_constructor(simplejson.dumps(val)).hexdigest() 37 | 38 | If you use a dict-like object, make sure you set ``sort_keys=True`` 39 | when calling ``json.dumps``. 40 | 41 | 42 | Synchronising processes 43 | ----------------------- 44 | 45 | The simplest way to ensure a valid model class is provided to a view is to 46 | validate the hash every time it is accessed. 47 | This means, each time a view would like to use the dynamic model class, 48 | the factory function checks the hash against one stored in a shared data store. 49 | Whenever a model class is generated, the shared store's hash is updated. 50 | 51 | Generally you will use something fast for this, 52 | for example memcached or redis. 53 | As long as all processes have access to the same data store, this approach 54 | will work. 55 | 56 | Of course, there can be race conditions. 57 | If generating the dynamic model class takes longer in one process then its 58 | hash may overwrite that from a more recent version. 59 | 60 | In that case, the only prevention may be to either using the database as a 61 | shared store, keeping all related changes to the one transaction, or by 62 | manually implementing a locking strategy. 63 | 64 | Dynamic models are surprisingly stable when the definitions change rarely. 65 | But I cannot vouch for their robustness where migrations are often occuring, 66 | in more than one process or thread. 67 | -------------------------------------------------------------------------------- /doc/topics/model.rst: -------------------------------------------------------------------------------- 1 | .. _topics-model: 2 | 3 | ================================ 4 | Defining a dynamic model factory 5 | ================================ 6 | 7 | The basic principle that allows us to create dynamic classes is the built-in 8 | function ``type()``. 9 | Instead of the normal syntax to define a class in Python: 10 | 11 | .. code-block:: python 12 | 13 | class Person(object): 14 | name = "Julia" 15 | 16 | The ``type()`` function can be used to create the same class, here is how 17 | the class above looks using the ``type()`` built-in: 18 | 19 | .. code-block:: python 20 | 21 | Person = type("Person", (object,), {'name': "Julia"}) 22 | 23 | Using ``type()`` means you can programatically determine the number and 24 | names of the attributes that make up the class. 25 | 26 | 27 | Django models 28 | ------------- 29 | 30 | Django models can be essentially defined in the same manner, with the one 31 | additional requirement that you need to define an attribute called 32 | ``__module__``. Here is a simple Django model: 33 | 34 | .. code-block:: python 35 | 36 | class Animal(models.Model): 37 | name = models.CharField(max_length=32) 38 | 39 | And here is the equivalent class built using ``type()``: 40 | 41 | .. code-block:: python 42 | 43 | attrs = { 44 | 'name': models.CharField(max_length=32), 45 | '__module__': 'myapp.models' 46 | } 47 | Animal = type("Animal", (models.Model,), attrs) 48 | 49 | Any Django model that can be defined in the normal fashion can be 50 | made using ``type()``. 51 | 52 | 53 | Django's model cache 54 | -------------------- 55 | 56 | Django automatically caches model classes when you subclass ``models.Model``. 57 | If you are generating a model that has a name that may already exist, you should 58 | firstly remove the existing cached class. 59 | 60 | There is no official, documented way to do this, but current versions of Django 61 | allow you to delete the cache entry directly: 62 | 63 | .. code-block:: python 64 | 65 | from django.db.models.loading import cache 66 | try: 67 | del cache.app_models[appname][modelname] 68 | except KeyError: 69 | pass 70 | 71 | .. note:: 72 | 73 | When using Django in non-official or undocumented ways, it's highly 74 | advisable to write unit tests to ensure that the code does what you 75 | indend it to do. This is especially useful when upgrading Django in 76 | the future, to ensure that all uses of undocumented features still 77 | work with the new version of Django. 78 | 79 | 80 | Using the model API 81 | ------------------- 82 | 83 | Because the names of model fields may no longer be known to the developer, 84 | it makes using Django's model API a little more difficult. 85 | There are at least three simple approaches to this problem. 86 | 87 | Firstly, you can use Python's ``**`` syntax to pass a mapping object as 88 | a set of keyword arguments. 89 | This is not as elegant as the normal syntax, but does the job: 90 | 91 | .. code-block:: python 92 | 93 | kwargs = {'name': "Jenny", 'color': "Blue"} 94 | print People.objects.filter(**kwargs) 95 | 96 | A second approach is to subclass ``django.db.models.query.QuerySet`` and provide your own 97 | customisations to keep things clean. 98 | You can attach the customised ``QuerySet`` class by overloading the ``get_query_set`` 99 | method of your model manager. 100 | Beware however of making things too nonstandard, forcing other developers to 101 | learn your new API. 102 | 103 | .. code-block:: python 104 | 105 | from django.db.models.query import QuerySet 106 | from django.db import models 107 | 108 | class MyQuerySet(QuerySet): 109 | def filter(self, *args, **kwargs): 110 | kwargs.update((args[i],args[i+1]) for i in range(0, len(args), 2)) 111 | return super(MyQuerySet, self).filter(**kwargs) 112 | 113 | class MyManager(models.Manager): 114 | def get_query_set(self): 115 | return MyQuerySet(self.model) 116 | 117 | # XXX Add the manager to your dynamic model... 118 | 119 | # Warning: This project uses a customised filter method! 120 | print People.objects.filter(name="Jenny").filter('color', 'blue') 121 | 122 | A third approach is to simply provide a helper function that creates either a 123 | preprepared ``kwargs`` mapping or returns a ``django.db.models.Q`` object, which 124 | can be fed directly to a queryset as seen above. This would be like creating a 125 | new API, but is a little more explicit than subclassing ``QuerySet``. 126 | 127 | .. code-block:: python 128 | 129 | from django.db.models import Q 130 | 131 | def my_query(*args, **kwargs): 132 | """ turns my_query(key, val, key, val, key=val) into a Q object. """ 133 | kwargs.update((args[i],args[i+1]) for i in range(0, len(args), 2)) 134 | return Q(**kwargs) 135 | 136 | print People.objects.filter(my_query('color', 'blue', name="Jenny")) 137 | 138 | 139 | What comes next? 140 | ---------------- 141 | 142 | Although this is enough to define a Django model class, 143 | if the model isn't in existence when ``syncdb`` is run, 144 | no respective database tables will be created. 145 | The creation and migration of database tables is covered in 146 | :ref:`database migration `. 147 | 148 | Also relevant is the appropriately time regeneration of the model class, 149 | (`especially` if you want to host using more than one server) 150 | see :ref:`model migration ` and, if you would like 151 | to edit the dynamic models in Django's admin, 152 | :ref:`admin migration `. 153 | 154 | 155 | -------------------------------------------------------------------------------- /epio.ini: -------------------------------------------------------------------------------- 1 | [wsgi] 2 | requirements = requirements.txt 3 | 4 | 5 | [static] 6 | /static = static_root 7 | 8 | 9 | [services] 10 | postgres = true 11 | redis = true 12 | 13 | 14 | [checkout] 15 | directory_name = dynamic_models 16 | 17 | 18 | [env] 19 | #IN_PRODUCTION = true 20 | DJANGO_SETTINGS_MODULE = dynamic_models.settings.epio 21 | 22 | 23 | [symlinks] 24 | # Any symlinks you'd like to add. As an example, link 'config.py' to 'configs/epio.py' 25 | # config.py = configs/epio.py 26 | 27 | 28 | [django] 29 | base = . 30 | append_settings = false 31 | 32 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | import imp 4 | try: 5 | imp.find_module('settings') # Assumed to be in the same directory. 6 | except ImportError: 7 | import sys 8 | 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" % __file__) 9 | sys.exit(1) 10 | 11 | import settings 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django>=1.2 2 | South>=0.7 3 | django-redis-cache 4 | -------------------------------------------------------------------------------- /responses/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willhardy/dynamic-models/12327589973ea1492a4b54922c90a0b479188e20/responses/__init__.py -------------------------------------------------------------------------------- /responses/models.py: -------------------------------------------------------------------------------- 1 | # This is a placeholder app, to hold all of the dynamic survey response models 2 | # from the surveymaker app. This was recommended on the Django wiki, and I 3 | # haven't yet confirmed if this is necessary. 4 | -------------------------------------------------------------------------------- /settings/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | from settings.default import * 4 | -------------------------------------------------------------------------------- /settings/default.py: -------------------------------------------------------------------------------- 1 | # Django settings for dynamic_models project. 2 | 3 | import os 4 | PROJECT_DIR = os.path.join(os.path.dirname(__file__), '..') 5 | project_dir = lambda p: os.path.join(PROJECT_DIR, p) 6 | 7 | 8 | DEBUG = True 9 | TEMPLATE_DEBUG = DEBUG 10 | 11 | ADMINS = ( 12 | ('Will Hardy', 'dynamic_models@willhardy.com.au'), 13 | ) 14 | 15 | MANAGERS = ADMINS 16 | 17 | DATABASES = { 18 | 'default': { 19 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 20 | 'NAME': 'test.db', # Or path to database file if using sqlite3. 21 | 'USER': '', # Not used with sqlite3. 22 | 'PASSWORD': '', # Not used with sqlite3. 23 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 24 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 25 | } 26 | } 27 | 28 | # Local time zone for this installation. Choices can be found here: 29 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 30 | # although not all choices may be available on all operating systems. 31 | # On Unix systems, a value of None will cause Django to use the same 32 | # timezone as the operating system. 33 | # If running in a Windows environment this must be set to the same as your 34 | # system time zone. 35 | TIME_ZONE = 'Europe/Amsterdam' 36 | 37 | # Language code for this installation. All choices can be found here: 38 | # http://www.i18nguy.com/unicode/language-identifiers.html 39 | LANGUAGE_CODE = 'en-au' 40 | 41 | SITE_ID = 1 42 | 43 | # If you set this to False, Django will make some optimizations so as not 44 | # to load the internationalization machinery. 45 | USE_I18N = True 46 | 47 | # If you set this to False, Django will not format dates, numbers and 48 | # calendars according to the current locale 49 | USE_L10N = True 50 | 51 | # Absolute filesystem path to the directory that will hold user-uploaded files. 52 | # Example: "/home/media/media.lawrence.com/media/" 53 | MEDIA_ROOT = project_dir('media') 54 | 55 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 56 | # trailing slash. 57 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 58 | MEDIA_URL = '/media/' 59 | 60 | # Absolute path to the directory static files should be collected to. 61 | # Don't put anything in this directory yourself; store your static files 62 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 63 | # Example: "/home/media/media.lawrence.com/static/" 64 | STATIC_ROOT = project_dir('static_root') 65 | 66 | # URL prefix for static files. 67 | # Example: "http://media.lawrence.com/static/" 68 | STATIC_URL = '/static/' 69 | 70 | # URL prefix for admin static files -- CSS, JavaScript and images. 71 | # Make sure to use a trailing slash. 72 | # Examples: "http://foo.com/static/admin/", "/static/admin/". 73 | ADMIN_MEDIA_PREFIX = '/static/admin/' 74 | 75 | # Additional locations of static files 76 | STATICFILES_DIRS = ( 77 | project_dir('static'), 78 | ) 79 | 80 | # List of finder classes that know how to find static files in 81 | # various locations. 82 | STATICFILES_FINDERS = ( 83 | 'django.contrib.staticfiles.finders.FileSystemFinder', 84 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 85 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 86 | ) 87 | 88 | # Make this unique, and don't share it with anybody. 89 | SECRET_KEY = 'j#amlng(qj_byjnbdgdx^@9la&996zozl+k#5usab2sp%!t3eb' 90 | 91 | # List of callables that know how to import templates from various sources. 92 | TEMPLATE_LOADERS = ( 93 | 'django.template.loaders.filesystem.Loader', 94 | 'django.template.loaders.app_directories.Loader', 95 | # 'django.template.loaders.eggs.Loader', 96 | ) 97 | 98 | MIDDLEWARE_CLASSES = ( 99 | 'django.middleware.common.CommonMiddleware', 100 | 'django.contrib.sessions.middleware.SessionMiddleware', 101 | 'django.middleware.csrf.CsrfViewMiddleware', 102 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 103 | 'django.contrib.messages.middleware.MessageMiddleware', 104 | ) 105 | 106 | ROOT_URLCONF = 'dynamic_models.urls' 107 | 108 | CACHES = { 109 | 'default': { 110 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 111 | } 112 | } 113 | 114 | 115 | TEMPLATE_DIRS = ( 116 | project_dir('templates'), 117 | ) 118 | 119 | INSTALLED_APPS = ( 120 | 'django.contrib.auth', 121 | 'django.contrib.contenttypes', 122 | 'django.contrib.sessions', 123 | 'django.contrib.sites', 124 | 'django.contrib.messages', 125 | 'django.contrib.staticfiles', 126 | 'django.contrib.admin', 127 | 'surveymaker', 128 | ) 129 | 130 | # A sample logging configuration. The only tangible logging 131 | # performed by this configuration is to send an email to 132 | # the site admins on every HTTP 500 error. 133 | # See http://docs.djangoproject.com/en/dev/topics/logging for 134 | # more details on how to customize your logging configuration. 135 | LOGGING = { 136 | 'version': 1, 137 | 'disable_existing_loggers': False, 138 | 'handlers': { 139 | 'mail_admins': { 140 | 'level': 'ERROR', 141 | 'class': 'django.utils.log.AdminEmailHandler' 142 | }, 143 | 'console': { 144 | 'level':'DEBUG', 145 | 'class':'logging.StreamHandler', 146 | }, 147 | }, 148 | 'loggers': { 149 | 'django.request': { 150 | 'handlers': ['mail_admins'], 151 | 'level': 'ERROR', 152 | 'propagate': True, 153 | }, 154 | 'surveymaker': { 155 | 'handlers': ['console'], 156 | 'level': 'DEBUG', 157 | 'propagate': True, 158 | }, 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /surveymaker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willhardy/dynamic-models/12327589973ea1492a4b54922c90a0b479188e20/surveymaker/__init__.py -------------------------------------------------------------------------------- /surveymaker/admin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | from django.contrib import admin 5 | from django.db.models.signals import post_save 6 | 7 | from . import models 8 | from . import utils 9 | 10 | class QuestionInline(admin.TabularInline): 11 | model = models.Question 12 | 13 | class SurveyAdmin(admin.ModelAdmin): 14 | inlines = [QuestionInline] 15 | 16 | admin.site.register(models.Survey, SurveyAdmin) 17 | 18 | # Go through all the current loggers in the database, and register an admin 19 | for survey in models.Survey.objects.all(): 20 | utils.reregister_in_admin(admin.site, survey.Response) 21 | 22 | # Update definitions when they change 23 | def survey_post_save(sender, instance, created, **kwargs): 24 | utils.reregister_in_admin(admin.site, instance.Response) 25 | post_save.connect(survey_post_save, sender=models.Survey) 26 | 27 | -------------------------------------------------------------------------------- /surveymaker/dynamic_models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import logging 3 | 4 | from django.db import models 5 | from django.utils.hashcompat import md5_constructor 6 | from django.core.cache import cache 7 | 8 | from . import utils 9 | 10 | def get_survey_response_model(survey, regenerate=False, notify_changes=True): 11 | """ Takes a survey object and returns a model for survey responses. 12 | Setting regenerate forces a regeneration, regardless of cached models. 13 | Setting notify_changes updates the cache with the current hash. 14 | """ 15 | name = filter(str.isalpha, survey.slug.encode('ascii', 'ignore')) 16 | _app_label = 'responses' 17 | _model_name = 'Response'+name 18 | 19 | # Skip regeneration if we have a valid cached model 20 | cached_model = utils.get_cached_model(_app_label, _model_name, regenerate) 21 | if cached_model is not None: 22 | return cached_model 23 | 24 | # Collect the dynamic model's class attributes 25 | attrs = { 26 | '__module__': __name__, 27 | '__unicode__': lambda s: '%s response' % name 28 | } 29 | 30 | class Meta: 31 | app_label = 'responses' 32 | verbose_name = survey.name + ' Response' 33 | attrs['Meta'] = Meta 34 | 35 | # Add a field for each question 36 | questions = survey.question_set.all() 37 | for question in questions: 38 | field_name = question.slug.replace('-','_').encode('ascii', 'ignore') 39 | attrs[field_name] = question.get_field() 40 | 41 | # Add a hash representing this model to help quickly identify changes 42 | attrs['_hash'] = generate_model_hash(survey) 43 | 44 | # A convenience function for getting the data in a predictablly ordered tuple 45 | attrs['data'] = property(lambda s: tuple(getattr(s, q.slug) for q in questions)) 46 | 47 | model = type('Response'+name, (models.Model,), attrs) 48 | 49 | # You could create the table and columns here if you're paranoid that it 50 | # hasn't happened yet. 51 | #utils.create_db_table(model) 52 | # Be wary though, that you won't be able to rename columns unless you 53 | # prevent the following line from being run. 54 | #utils.add_necessary_db_columns(model) 55 | 56 | if notify_changes: 57 | utils.notify_model_change(model) 58 | 59 | return model 60 | 61 | 62 | def build_existing_survey_response_models(): 63 | """ Builds all existing dynamic models at once. """ 64 | # To avoid circular imports, the model is retrieved from the model cache 65 | Survey = models.get_model('surveymaker', 'Survey') 66 | for survey in Survey.objects.all(): 67 | Response = get_survey_response_model(survey) 68 | # Create the table if necessary, shouldn't be necessary anyway 69 | utils.create_db_table(Response) 70 | # While we're at it... 71 | utils.add_necessary_db_columns(Response) 72 | 73 | 74 | def generate_model_hash(survey): 75 | """ Take a survey object and generate a suitable hash for the relevant 76 | aspect of responses model. 77 | For our survey model, a list of the question slugs 78 | """ 79 | return md5_constructor(survey.get_hash_string()).hexdigest() 80 | 81 | -------------------------------------------------------------------------------- /surveymaker/fields.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from decimal import Decimal 3 | 4 | from django.db import models 5 | 6 | 7 | # Callables that return a django model field, they will be mapped to 8 | # available field types. 9 | # They must accept arguments such as blank and choices 10 | 11 | def get_decimal_field(**kwargs): 12 | kwargs.setdefault('max_digits', 6) 13 | kwargs.setdefault('decimal_places', 2) 14 | kwargs.setdefault('null', True) 15 | if 'choices' in kwargs: 16 | kwargs['choices'] = [(Decimal(k),v) for k,v in kwargs['choices']] 17 | return models.DecimalField(**kwargs) 18 | 19 | def get_char_field(**kwargs): 20 | kwargs.setdefault('max_length', 255) 21 | kwargs.setdefault('default', "") 22 | return models.CharField(**kwargs) 23 | 24 | def get_text_field(**kwargs): 25 | kwargs.setdefault('default', "") 26 | return models.TextField(**kwargs) 27 | 28 | def get_integer_field(**kwargs): 29 | kwargs.setdefault('null', True) 30 | if 'choices' in kwargs: 31 | kwargs['choices'] = [(int(k),v) for k,v in kwargs['choices']] 32 | return models.IntegerField(**kwargs) 33 | 34 | 35 | ANSWER_FIELDS = { 36 | 'ShortText': get_char_field, 37 | 'LongText': get_text_field, 38 | 'Integer': get_integer_field, 39 | 'Decimal': get_decimal_field, 40 | } 41 | 42 | ANSWER_TYPES = ( 43 | ('ShortText', 'Short text'), 44 | ('LongText', 'Long text'), 45 | ('Integer', 'Number'), 46 | ('Decimal', 'Decimal number'), 47 | ) 48 | 49 | -------------------------------------------------------------------------------- /surveymaker/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [{"pk": 1, "model": "auth.user", "fields": {"username": "will", "first_name": "", "last_name": "", "is_active": true, "is_superuser": true, "is_staff": true, "last_login": "2011-06-07 16:33:13", "groups": [], "user_permissions": [], "password": "sha1$f0dd4$04c6f01be4923a16413aec3f9abd91b45ef01256", "email": "will@willhardy.com.au", "date_joined": "2011-06-07 16:33:13"}}] -------------------------------------------------------------------------------- /surveymaker/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | from django.core.exceptions import ValidationError 4 | from django.db import models 5 | from django.db.models.signals import post_save, pre_save, pre_delete, post_delete 6 | from django.utils import simplejson 7 | 8 | from . import fields 9 | from . import utils 10 | from . import signals 11 | from .dynamic_models import get_survey_response_model, build_existing_survey_response_models 12 | 13 | 14 | # Build all existing survey response models as soon as possible 15 | # This is optional, but is nice as it avoids building the model when the 16 | # first relevant view is loaded. 17 | utils.when_classes_prepared('surveymaker', ['Survey', 'Question'], 18 | build_existing_survey_response_models) 19 | 20 | 21 | class Survey(models.Model): 22 | name = models.CharField(max_length=255, default="") 23 | slug = models.SlugField(unique=True) 24 | 25 | def __unicode__(self): 26 | return self.name 27 | 28 | def clean(self): 29 | if not self.slug.isalpha(): 30 | raise ValidationError("Please leave out your non-alpha chars for this slug.") 31 | if Survey.objects.filter(pk=self.pk).exclude(slug=self.slug).exists(): 32 | raise ValidationError("This is just a simple example, please don't go rename the slug.") 33 | 34 | @property 35 | def Response(self): 36 | " Convenient access the relevant model class for the responses " 37 | return get_survey_response_model(self) 38 | 39 | def get_survey_response_model(self, regenerate=False, notify_changes=True): 40 | return get_survey_response_model(self, regenerate=regenerate, notify_changes=notify_changes) 41 | 42 | def get_hash_string(self): 43 | """ Return a string to describe the parts of the questions that are 44 | relevant to the generated dynamic model (the Response model) 45 | """ 46 | # Only use the fields that are relevant 47 | val = [(q.slug, q.required, q.question, q.choices, q.rank) for q in self.question_set.all()] 48 | return simplejson.dumps(val) 49 | 50 | 51 | class Question(models.Model): 52 | survey = models.ForeignKey(Survey) 53 | question = models.CharField(max_length=255, default="") 54 | slug = models.SlugField() 55 | answer_type = models.CharField(max_length=32, choices=fields.ANSWER_TYPES) 56 | choices = models.CharField(max_length=1024, default="", blank=True, 57 | help_text="comma separated choices, keep them shortish") 58 | required = models.BooleanField(default=False) 59 | rank = models.PositiveIntegerField(default=5) 60 | 61 | def get_field(self): 62 | kwargs = {} 63 | kwargs['blank'] = not self.required 64 | kwargs['verbose_name'] = self.question 65 | if self.choices.strip(): 66 | kwargs['choices'] = [(x.strip(), x.strip()) for x in self.choices.split(",")] 67 | 68 | try: 69 | return fields.ANSWER_FIELDS[self.answer_type](**kwargs) 70 | except KeyError: 71 | return None 72 | 73 | def clean(self): 74 | if not all(x.isalpha() or x in "_" for x in self.slug) or not self.slug[0].isalpha(): 75 | raise ValidationError("Please use only alpha/underscore characters in this slug.") 76 | if self.answer_type == "Choice" and not self.choices.strip(): 77 | raise ValidationError("Choice type requires some choices") 78 | 79 | class Meta: 80 | ordering = ['rank'] 81 | unique_together = ['survey', 'slug'] 82 | 83 | 84 | # Connect signals 85 | pre_save.connect(signals.question_pre_save, sender=Question) 86 | post_save.connect(signals.question_post_save, sender=Question) 87 | post_delete.connect(signals.question_post_delete, sender=Question) 88 | post_save.connect(signals.survey_post_save, sender=Survey) 89 | pre_delete.connect(signals.survey_pre_delete, sender=Survey) 90 | 91 | -------------------------------------------------------------------------------- /surveymaker/signals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | from django.contrib import admin 5 | from django.core.exceptions import ObjectDoesNotExist 6 | 7 | from . import utils 8 | 9 | 10 | def question_pre_save(sender, instance, **kwargs): 11 | """ An optional signal to detect renamed slugs. 12 | This will rename the column so that the data is migrated. 13 | """ 14 | Question = sender 15 | try: 16 | # Try to detect if a question has been given a new slug (which would 17 | # require a column rename) 18 | if instance.pk: 19 | instance._old_slug = Question.objects.filter(pk=instance.pk).exclude(slug=instance.slug).get().slug 20 | 21 | # Fixture loading will not have a related survey object, so we can't use it 22 | # This won't be a problem because we're only looking for renamed slugs 23 | except ObjectDoesNotExist: 24 | pass 25 | 26 | 27 | def question_post_save(sender, instance, created, **kwargs): 28 | """ Adapt tables to any relavent changes: 29 | If the question slug has been renamed, rename the database column. 30 | """ 31 | try: 32 | # Regenerate our response model, which may have changed 33 | Response = instance.survey.get_survey_response_model(regenerate=True, notify_changes=False) 34 | 35 | # If we previously identified a renamed slug, then rename the column 36 | if hasattr(instance, '_old_slug'): 37 | utils.rename_db_column(Response, instance._old_slug, instance.slug) 38 | del instance._old_slug 39 | 40 | # If necessary, add any new columns 41 | utils.add_necessary_db_columns(Response) 42 | 43 | # Reregister the Survey model in the admin 44 | utils.reregister_in_admin(admin.site, Response) 45 | 46 | # Tell other process to regenerate their models 47 | utils.notify_model_change(Response) 48 | 49 | except ObjectDoesNotExist: 50 | return 51 | 52 | 53 | def question_post_delete(sender, instance, **kwargs): 54 | """ If you delete a question from a survey, update the model. 55 | """ 56 | Response = instance.survey.get_survey_response_model(regenerate=True, notify_changes=True) 57 | 58 | 59 | def survey_post_save(sender, instance, created, **kwargs): 60 | """ Ensure that a table exists for this logger. """ 61 | 62 | # Force our response model to regenerate 63 | Response = instance.get_survey_response_model(regenerate=True, notify_changes=False) 64 | 65 | # Create a new table if it's missing 66 | utils.create_db_table(Response) 67 | 68 | # Reregister the model in the admin 69 | utils.reregister_in_admin(admin.site, Response) 70 | 71 | # Tell other process to regenerate their models 72 | utils.notify_model_change(Response) 73 | 74 | 75 | def survey_pre_delete(sender, instance, **kwargs): 76 | Response = instance.Response 77 | 78 | # delete the data tables? (!) 79 | #utils.delete_db_table(Response) 80 | 81 | # unregister from the admin site 82 | utils.unregister_from_admin(admin.site, Response) 83 | 84 | 85 | -------------------------------------------------------------------------------- /surveymaker/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { font-family: sans-serif; color: #444; padding: 20px; } 2 | h2 { border-top: 1px solid #bbb; margin-top: 40px; padding: 10px 0;} 3 | table.results { border: 1px solid #999; border-collapse: collapse;} 4 | table.results th { background: #444; color: #fff; padding: 4px 10px;} 5 | table.results td { padding: 2px 10px; } 6 | ul.survey { margin-bottom: 30px; list-style: none; padding: 0;} 7 | ul.survey li { margin: 0 0 20px 0; } 8 | ul.survey label { display: block; font-weight: normal; } 9 | -------------------------------------------------------------------------------- /surveymaker/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dynamic Models Demonstration 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | {% block header %} 14 |

Dynamic Models Demonstration

15 |

This demonstration simply displays all data from survey responses at once, to see the structure and data of all tables.

16 | 20 | {% endblock header %} 21 |
22 | 23 |
24 | {% block content %} 25 | {% for survey,responses in surveys %} 26 |

{{ survey }}

27 | 28 | {% for question in survey.question_set.all %}{% endfor %} 29 | {% for response in responses %} 30 | {% for d in response.data %}{% endfor %} 31 | {% endfor %} 32 |
{{ question.slug }}
{{ d }}
33 | {% endfor %} 34 | 35 | {% endblock content %} 36 |
37 | 38 |
39 | 40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /surveymaker/templates/surveymaker/all.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% for survey,responses in surveys %} 5 |

{{ survey }}

6 | 7 | {% for question in survey.question_set.all %}{% endfor %} 8 | {% for response in responses %} 9 | {% for d in response.data %}{% endfor %} 10 | {% endfor %} 11 |
{{ question.slug }}
{{ d }}
12 | {% endfor %} 13 | 14 | {% endblock content %} 15 | -------------------------------------------------------------------------------- /surveymaker/templates/surveymaker/survey_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

Take the survey: {{ survey }}

5 |
6 | {% csrf_token %} 7 |
    8 | {{ form.as_ul }} 9 |
10 | 11 |
12 | {% endblock content %} 13 | -------------------------------------------------------------------------------- /surveymaker/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | from django.db import connection, DatabaseError 5 | from django.db import models 6 | from django.contrib.admin.sites import NotRegistered 7 | from django.db.models.signals import class_prepared 8 | from django.db.models.loading import cache as app_cache 9 | 10 | from django.core.urlresolvers import clear_url_caches 11 | from django.utils.importlib import import_module 12 | from django.core.cache import cache 13 | from django.conf import settings 14 | 15 | import logging 16 | from south.db import db 17 | 18 | logger = logging.getLogger('surveymaker') 19 | 20 | 21 | def unregister_from_admin(admin_site, model): 22 | " Removes the dynamic model from the given admin site " 23 | 24 | # First deregister the current definition 25 | # This is done "manually" because model will be different 26 | # db_table is used to check for class equivalence. 27 | for reg_model in admin_site._registry.keys(): 28 | if model._meta.db_table == reg_model._meta.db_table: 29 | del admin_site._registry[reg_model] 30 | 31 | # Try the normal approach too 32 | try: 33 | admin_site.unregister(model) 34 | except NotRegistered: 35 | pass 36 | 37 | # Reload the URL conf and clear the URL cache 38 | # It's important to use the same string as ROOT_URLCONF 39 | reload(import_module(settings.ROOT_URLCONF)) 40 | clear_url_caches() 41 | 42 | 43 | def reregister_in_admin(admin_site, model, admin_class=None): 44 | " (re)registers a dynamic model in the given admin site " 45 | 46 | # We use our own unregister, to ensure that the correct 47 | # existing model is found 48 | # (Django's unregister doesn't expect the model class to change) 49 | unregister_from_admin(admin_site, model) 50 | admin_site.register(model, admin_class) 51 | 52 | # Reload the URL conf and clear the URL cache 53 | # It's important to use the same string as ROOT_URLCONF 54 | reload(import_module(settings.ROOT_URLCONF)) 55 | clear_url_caches() 56 | 57 | 58 | def when_classes_prepared(app_name, dependencies, fn): 59 | """ Runs the given function as soon as the model dependencies are available. 60 | You can use this to build dyanmic model classes on startup instead of 61 | runtime. 62 | 63 | app_name the name of the relevant app 64 | dependencies a list of model names that need to have already been 65 | prepared before the dynamic classes can be built. 66 | fn this will be called as soon as the all required models 67 | have been prepared 68 | 69 | NB: The fn will be called as soon as the last required 70 | model has been prepared. This can happen in the middle of reading 71 | your models.py file, before potentially referenced functions have 72 | been loaded. Becaue this function must be called before any 73 | relevant model is defined, the only workaround is currently to 74 | move the required functions before the dependencies are declared. 75 | 76 | TODO: Allow dependencies from other apps? 77 | """ 78 | dependencies = [x.lower() for x in dependencies] 79 | 80 | def _class_prepared_handler(sender, **kwargs): 81 | """ Signal handler for class_prepared. 82 | This will be run for every model, looking for the moment when all 83 | dependent models are prepared for the first time. It will then run 84 | the given function, only once. 85 | """ 86 | sender_name = sender._meta.object_name.lower() 87 | already_prepared = set(app_cache.app_models.get(app_name,{}).keys() + [sender_name]) 88 | 89 | if (sender._meta.app_label == app_name and sender_name in dependencies 90 | and all([x in already_prepared for x in dependencies])): 91 | db.start_transaction() 92 | try: 93 | fn() 94 | except DatabaseError: 95 | # If tables are missing altogether, not much we can do 96 | # until syncdb/migrate is run. "The code must go on" in this 97 | # case, without running our function completely. At least 98 | # database operations will be rolled back. 99 | db.rollback_transaction() 100 | else: 101 | db.commit_transaction() 102 | # TODO Now that the function has been run, should/can we 103 | # disconnect this signal handler? 104 | 105 | # Connect the above handler to the class_prepared signal 106 | # NB: Although this signal is officially documented, the documentation 107 | # notes the following: 108 | # "Django uses this signal internally; it's not generally used in 109 | # third-party applications." 110 | class_prepared.connect(_class_prepared_handler, weak=False) 111 | 112 | 113 | def get_cached_model(app_label, model_name, regenerate=False, get_local_hash=lambda i: i._hash): 114 | 115 | # If this model has already been generated, we'll find it here 116 | previous_model = models.get_model(app_label, model_name) 117 | 118 | # Before returning our locally cached model, check that it is still current 119 | if previous_model is not None and not regenerate: 120 | CACHE_KEY = utils.HASH_CACHE_TEMPLATE % (app_label, model_name) 121 | if cache.get(CACHE_KEY) != get_local_hash(previous_model): 122 | logging.debug("Local and shared dynamic model hashes are different: %s (local) %s (shared)" % (get_local_hash(previous_model), cache.get(CACHE_KEY))) 123 | regenerate = True 124 | 125 | # We can force regeneration by disregarding the previous model 126 | if regenerate: 127 | previous_model = None 128 | # Django keeps a cache of registered models, we need to make room for 129 | # our new one 130 | utils.remove_from_model_cache(app_label, model_name) 131 | 132 | return previous_model 133 | 134 | 135 | def remove_from_model_cache(app_label, model_name): 136 | """ Removes the given model from the model cache. """ 137 | try: 138 | del app_cache.app_models[app_label][model_name.lower()] 139 | except KeyError: 140 | pass 141 | 142 | def create_db_table(model_class): 143 | """ Takes a Django model class and create a database table, if necessary. 144 | """ 145 | # XXX Create related tables for ManyToMany etc 146 | 147 | db.start_transaction() 148 | table_name = model_class._meta.db_table 149 | 150 | # Introspect the database to see if it doesn't already exist 151 | if (connection.introspection.table_name_converter(table_name) 152 | not in connection.introspection.table_names()): 153 | 154 | fields = _get_fields(model_class) 155 | 156 | db.create_table(table_name, fields) 157 | # Some fields are added differently, after table creation 158 | # eg GeoDjango fields 159 | db.execute_deferred_sql() 160 | logger.debug("Created table '%s'" % table_name) 161 | 162 | db.commit_transaction() 163 | 164 | 165 | def delete_db_table(model_class): 166 | table_name = model_class._meta.db_table 167 | db.start_transaction() 168 | db.delete_table(table_name) 169 | logger.debug("Deleted table '%s'" % table_name) 170 | db.commit_transaction() 171 | 172 | 173 | def _get_fields(model_class): 174 | """ Return a list of fields that require table columns. """ 175 | return [(f.name, f) for f in model_class._meta.local_fields] 176 | 177 | 178 | def add_necessary_db_columns(model_class): 179 | """ Creates new table or relevant columns as necessary based on the model_class. 180 | No columns or data are renamed or removed. 181 | This is available in case a database exception occurs. 182 | """ 183 | db.start_transaction() 184 | 185 | # Create table if missing 186 | create_db_table(model_class) 187 | 188 | # Add field columns if missing 189 | table_name = model_class._meta.db_table 190 | fields = _get_fields(model_class) 191 | db_column_names = [row[0] for row in connection.introspection.get_table_description(connection.cursor(), table_name)] 192 | 193 | for field_name, field in fields: 194 | if field.column not in db_column_names: 195 | logger.debug("Adding field '%s' to table '%s'" % (field_name, table_name)) 196 | db.add_column(table_name, field_name, field) 197 | 198 | 199 | # Some columns require deferred SQL to be run. This was collected 200 | # when running db.add_column(). 201 | db.execute_deferred_sql() 202 | 203 | db.commit_transaction() 204 | 205 | 206 | def rename_db_column(model_class, old_name, new_name): 207 | """ Rename a sensor's database column. """ 208 | table_name = model_class._meta.db_table 209 | db.start_transaction() 210 | db.rename_column(table_name, old_name, new_name) 211 | logger.debug("Renamed column '%s' to '%s' on %s" % (old_name, new_name, table_name)) 212 | db.commit_transaction() 213 | 214 | 215 | def notify_model_change(model): 216 | """ Notifies other processes that a dynamic model has changed. 217 | This should only ever be called after the required database changes have been made. 218 | """ 219 | CACHE_KEY = HASH_CACHE_TEMPLATE % (model._meta.app_label, model._meta.object_name) 220 | cache.set(CACHE_KEY, model._hash) 221 | logger.debug("Setting \"%s\" hash to: %s" % (model._meta.verbose_name, model._hash)) 222 | 223 | 224 | HASH_CACHE_TEMPLATE = 'dynamic_model_hash_%s-%s' 225 | -------------------------------------------------------------------------------- /surveymaker/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | from .models import Survey 4 | 5 | from django import forms 6 | from django.shortcuts import render_to_response, get_object_or_404, redirect 7 | from django.template.context import RequestContext 8 | 9 | def all_survey_responses(request): 10 | template_name = "surveymaker/all.html" 11 | 12 | surveys = [(survey, survey.Response.objects.all()) 13 | for survey in Survey.objects.all()] 14 | return render_to_response(template_name, {'surveys': surveys}, 15 | context_instance=RequestContext(request)) 16 | 17 | 18 | def get_response_form(response): 19 | class FormMeta: 20 | model = response 21 | return type('ResponseForm', (forms.ModelForm,), {'Meta': FormMeta}) 22 | 23 | 24 | def survey_form(request, survey_slug): 25 | template_name = "surveymaker/survey_form.html" 26 | survey = get_object_or_404(Survey, slug=survey_slug) 27 | Response = survey.Response 28 | ResponseForm = get_response_form(Response) 29 | 30 | if request.method == "POST": 31 | form = ResponseForm(request.POST) 32 | if form.is_valid(): 33 | form.save() 34 | return redirect('surveymaker_index') 35 | else: 36 | form = ResponseForm() 37 | 38 | return render_to_response(template_name, {'form': form, 'survey': survey}, 39 | context_instance=RequestContext(request)) 40 | 41 | -------------------------------------------------------------------------------- /urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, include, url 2 | 3 | from django.contrib import admin 4 | admin.autodiscover() 5 | 6 | urlpatterns = patterns('', 7 | url(r'^$', 'surveymaker.views.all_survey_responses', name='surveymaker_index'), 8 | url(r'^(?P.*)/new/$', 'surveymaker.views.survey_form', name='surveymaker_form'), 9 | url(r'^admin/', include(admin.site.urls)), 10 | ) 11 | --------------------------------------------------------------------------------