diff --git a/.gitignore b/.gitignore index a8ba298..9d11abe 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.o *.so _site/ +_build/ # Packages # ############ @@ -20,7 +21,6 @@ _site/ *.rar *.tar *.zip - # Logs and databases # ###################### *.log @@ -48,6 +48,8 @@ Thumbs.db __pycache__/ *.py[cod] .env +.eggs/ +.egg/ # Django # ################# diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..f7deb36 --- /dev/null +++ b/.mailmap @@ -0,0 +1,3 @@ +Ross M Karchner +Ross M Karchner +Scott Cranfill diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..06905eb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: python +install: + - pip install tox +script: + - tox +env: + - TOXENV=py27 + - TOXENV=py33 + - TOXENV=py34 diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..51940ad --- /dev/null +++ b/AUTHORS @@ -0,0 +1,10 @@ +Ans +Anselm Bradford +Chris Contolini +James Wilson +Marc Esher +Ross M Karchner +Scott Cranfill +bill shelton +sheltonw +virtix diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index da5a178..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,17 +0,0 @@ -All notable changes to this project will be documented in this file. -We follow the [Semantic Versioning 2.0.0](http://semver.org/) format. - - -## x.y.z - YYYY-MM-DD - -### Added -- Lorem ipsum dolor sit amet - -### Deprecated -- Nothing. - -### Removed -- Nothing. - -### Fixed -- Nothing. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 059f2ec..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,32 +0,0 @@ -# Guidance on how to contribute - -> All contributions to this project will be released under the CC0 public domain -> dedication. By submitting a pull request or filing a bug, issue, or -> feature request, you are agreeing to comply with this waiver of copyright interest. -> Details can be found in our [TERMS](TERMS.md) and [LICENCE](LICENSE). - - -There are two primary ways to help: - - Using the issue tracker, and - - Changing the code-base. - - -## Using the issue tracker - -Use the issue tracker to suggest feature requests, report bugs, and ask questions. -This is also a great way to connect with the developers of the project as well -as others who are interested in this solution. - -Use the issue tracker to find ways to contribute. Find a bug or a feature, mention in -the issue that you will take on that effort, then follow the _Changing the code-base_ -guidance below. - - -## Changing the code-base - -Generally speaking, you should fork this repository, make changes in your -own fork, and then submit a pull-request. All new code should have associated unit -tests that validate implemented features and the presence or lack of defects. -Additionally, the code should follow any stylistic and architectural guidelines -prescribed by the project. In the absence of such guidelines, mimic the styles -and patterns in the existing code-base. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..44c4b91 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,33 @@ +Guidance on how to contribute +============================= + + All contributions to this project will be released under the CC0 + public domain dedication. By submitting a pull request or filing a + bug, issue, or feature request, you are agreeing to comply with this + waiver of copyright interest. Details can be found in our + `TERMS `__ and `LICENCE `__. + +There are two primary ways to help: - Using the issue tracker, and - +Changing the code-base. + +Using the issue tracker +----------------------- + +Use the issue tracker to suggest feature requests, report bugs, and ask +questions. This is also a great way to connect with the developers of +the project as well as others who are interested in this solution. + +Use the issue tracker to find ways to contribute. Find a bug or a +feature, mention in the issue that you will take on that effort, then +follow the *Changing the code-base* guidance below. + +Changing the code-base +---------------------- + +Generally speaking, you should fork this repository, make changes in +your own fork, and then submit a pull-request. All new code should have +associated unit tests that validate implemented features and the +presence or lack of defects. Additionally, the code should follow any +stylistic and architectural guidelines prescribed by the project. In the +absence of such guidelines, mimic the styles and patterns in the +existing code-base. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..8cb76be --- /dev/null +++ b/ChangeLog @@ -0,0 +1,78 @@ +CHANGES +======= + +* delete unneccesary print statement +* add pytz +* Update README.rst +* fix date formatting filter, and implement relative template imports, and a global request object! +* fixed logic for certain types of 404's +* corrected README re: generic views +* refactored to simplify and unify the generic views, and make the combined SheerTemplateView behave more like Sheer does +* A new generic view that infers templates from the URL, some configuration simplification and DRY work, the beginning of a tool for populating a Django model from a Sheer template +* Update README.rst +* One last markdown-to-ReStructuredText conversion +* Update README.rst +* Update README.rst +* Update README.rst +* Removed redundant changelog +* Remove duplicate names from AUTHORS +* Removed screenshot, added mailmap +* The actual tests I meant to include in the last commit, plus some administrivia +* Migrated some unit tests from Sheer, and configs for tox and Travis +* request.GET.copy() doesn't get you a mutable object +* Up to date with @kwall's latest changes to sheer.query +* added jinja2 to the requirements +* add elasticsearch depenedency +* remove unneeded import, and fix reference to README in setup.cfg +* trying to get autodoc working +* moved doc stub into docs folder +* product of running sphinx-quickstart +* Update README.rst +* This is madness! This is Python! +* Filtering doesn't work yet, but now it fails differently +* Formatting fixes, and added information on SheerDetailView +* Guidance re: .permalink is no longer accurate +* - restores .permalink (using a registry mechanism) - provides a generic view which should suffice for most uses of sheer's "lookups" feature +* fix typo ;) +* initial commit +* Update README.md +* Update README.md +* Update README.md +* Update README.md +* Update README.md +* Update README.md +* Fix typo +* Update README.md +* Update README.md +* Update README.md +* Update README.md +* Update README.md +* Update README.md +* Update README.md +* Removes trailing newlines +* Update TERMS.md +* Fixes #13 - adds INSTALL.md +* Updating gitignore for more robust coverage +* Fixes typo +* Update README.md +* Add front-endy things to ignore +* Add open source checklist and to do item in README +* Use official CC0 plain text legal code +* Add suggested CHANGELOG format +* Add language about CFPB contribs. to a joint work +* https fix +* https a URL on README +* Fix typo in README +* Update first sentence of TERMS +* Copyedit README +* Copyedit CONTRIBUTING +* Fix typo in TERMS.md +* Fix typos in README. Add simple gitignore +* Update screenshot link to point to CFPB +* Adjust screenshot size +* Add screenshot to README +* Add example screenshot +* Add example screenshot +* Update README and add CHANGELOG +* Add TERMS and CONTRIBUTING +* Initial commit diff --git a/INSTALL.md b/INSTALL.md deleted file mode 100644 index 4c199e6..0000000 --- a/INSTALL.md +++ /dev/null @@ -1,3 +0,0 @@ -# Installation instructions - -Detailed instructions on how to install, configure, and get the project running. diff --git a/INSTALL.rst b/INSTALL.rst new file mode 100644 index 0000000..b2df9bd --- /dev/null +++ b/INSTALL.rst @@ -0,0 +1,5 @@ +Installation instructions +========================= + +Detailed instructions on how to install, configure, and get the project +running. diff --git a/README.md b/README.md deleted file mode 100644 index 8a1d0e9..0000000 --- a/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# django-sheerlike - -This is an attempt to port some of our favorite [sheer](https://github.com/cfpb/sheer) features over to Django. - -**Current status**: Not usable for any purpose. - -**Runs on**: Django 1.8 and Python 2.7 - -# Philosophy - -It's our goal to respect the work that people have put into building sites for Sheer, but also avoid coloring too far outside the lines of how Django works. - -# Required changes - -The biggest change is that the bundle of files that we were calling a "sheer site", is now best thought of as a set of templates for apps that should be defined in the proper Django form. [cfgov-refresh](https://github.com/cfpb/cfgov-refresh) describes many "apps" (blog, newsroom, activity feed, etc), while [Owning a Home](https://github.com/cfpb/owning-a-home/) probably only describes one. - -As stated in [Two Scoops of Django 1.8](http://twoscoopspress.org/products/two-scoops-of-django-1-8): - -_"each app should be tightly focused on its task. If an app can’t be explained in a single sentence of moderate length, or you need to say ‘and’ more than once, it probably means the app is too big and should be broken up."_ - -Sheer's URL routing goes away entirely. If a particular URL renders a particular template, it's because it was specified in a Django view. If a template presumes the existence of a "post" item on blog post detail, that object will have to be created in the Django view, and passed into the template context. This is pretty simple, though: - -```python -def blog_detail(request, slug): - post = get_document(doctype='posts', docid=slug) - return render(request, 'blog/_single.html', context={'post':post) -``` - -Almost all of the rest of the sheer machinery is still intact, though: you still have access to the global 'queries' object, and can still call functions like get_document and more_like_this. - -## Template tweaks - -Replace references to 'some_result.permalink' to use [Django's URL reversing system](https://docs.djangoproject.com/en/1.8/ref/urlresolvers/#django.core.urlresolvers.reverse). - -this: -``` - -``` - -becomes: -``` - -``` - -Eliminate relative template includes/imports. for example, in (cfgov-refresh) blog/index.html: - -`{% import "_vars-blog.html" as vars with context %}` - -becomes `{% import "blog/_vars-blog.html" as vars with context %}` - -The request object is a context variable now, so in order to reference it in 'imported' templates, [you must specify 'with context'](http://jinja.pocoo.org/docs/dev/templates/#import-context-behavior). - -For example, `{% from "macros.html" import share as share %}` becomes `{% from "macros.html" import share as share with context%}` - -Also, the [Django request object](https://docs.djangoproject.com/en/1.8/ref/request-response/#httprequest-objects) has different properties and methods than the one available in Flask/sheer. - -We'll probably find a few more things, so this list will grow. - -## API's and RSS Feeds - -We will need to switch to native Django tools for such things. - -# Recommendations - -- Look for opportunities to replace complicated template logic with python views -- Switch to [Django Pagination](https://docs.djangoproject.com/en/1.8/topics/pagination/) - ----- - -## Open source licensing info -1. [TERMS](TERMS.md) -2. [LICENSE](LICENSE) -3. [CFPB Source Code Policy](https://github.com/cfpb/source-code-policy/) diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..5fea441 --- /dev/null +++ b/README.rst @@ -0,0 +1,137 @@ +django-sheerlike +================ + +.. image:: https://travis-ci.org/cfpb/django-sheerlike.svg + :target: https://travis-ci.org/cfpb/django-sheerlike + +This is an attempt to port some of our favorite +`sheer `__ features over to Django. + +**Current status**: Not usable for any purpose. + +**Runs on**: Django 1.8 and Python 2.7 + +Philosophy +========== + +It's our goal to respect the work that people have put into building +sites for Sheer, but also avoid coloring too far outside the lines of +how Django works. + +Required changes +================ + +The biggest change is that the bundle of files that we were calling a +"sheer site", is now best thought of as a set of templates for apps that +should be defined in the proper Django form. +`cfgov-refresh `__ describes many +"apps" (blog, newsroom, activity feed, etc), while `Owning a +Home `__ probably only describes +one. + +As stated in `Two Scoops of Django +1.8 `__: + +*"each app should be tightly focused on its task. If an app can’t be +explained in a single sentence of moderate length, or you need to say +‘and’ more than once, it probably means the app is too big and should be +broken up."* + +Sheer's URL routing goes away entirely. If a particular URL renders a +particular template, it's because it was specified in a Django view. If +a template presumes the existence of a "post" item on blog post detail, +that object will have to be created in the Django view, and passed into +the template context. We've provided a generic view that makes this +pretty simple: + +.. code:: python + + url(r'^blog/(?P[\w-]+)/$', SheerTemplateView.as_view( + doc_type='posts', + local_name='post', + default_template='blog/_single.html', + ), name='blog_detail'), + +Almost all of the rest of the sheer machinery is still intact, though: +you still have access to the global 'queries' object, and can still call +functions like get\_document and more\_like\_this. + +Template tweaks +--------------- + +The `Django request +object `__ +has different properties and methods than the one available in +Flask/sheer. We've added some helpers to support existing access patterns, but those should be considered deprecated. + +Inline IF statements MUST have an else clause, `otherwise the output is +undefined `__ + +Old: + +:: + + {% macro format_phone(number) %} + {%- for char in number -%} + {{- '(' if loop.index == 1 -}} + {{ char }} + {{- ') ' if loop.index == 3 -}} + {{- '-' if loop.index == 6 -}} + {%- endfor %} + {% endmacro %} + +New: + +:: + + {% macro format_phone(number) %} + {%- for char in number -%} + {{- '(' if loop.index == 1 else '' -}} + {{ char }} + {{- ') ' if loop.index == 3 else '' -}} + {{- '-' if loop.index == 6 else '' -}} + {%- endfor %} + {% endmacro %} +` + +API's and RSS Feeds +------------------- + +We will need to switch to native Django tools for such things. + +See it in action +================ + +Want to test this out? + +These instructions assume you have a local elasticsearch server, already populated by 'sheer index' as documented in the `cfgov-refresh readme `__. + +- Check out `cfgov-django `__ alongside cfgov-refresh. +- create a new virtualenv and pip install -r requirements.txt +- cd into the 'cfgov' directory, and run './manage.py runserver' + +You should then be able to see the site running on http://localhost:8000 + +Run the tests +============= + +Install `tox `__ and run the 'tox' command from a checkout of this repo. + +Recommendations +=============== + +- Look for opportunities to replace complicated template logic with + python views +- Switch to `Django + Pagination `__ + +-------------- + +Open source licensing info +-------------------------- + +1. `TERMS `__ +2. `LICENSE `__ +3. `CFPB Source Code + Policy `__ + diff --git a/TERMS.md b/TERMS.md deleted file mode 100644 index f64c133..0000000 --- a/TERMS.md +++ /dev/null @@ -1,52 +0,0 @@ -As a work of the United States Government, this package (excluding any -exceptions listed below) is in the public domain within the United States. -Additionally, we waive copyright and related rights in the work worldwide -through the [CC0 1.0 Universal public domain dedication][CC0]. - -Software source code previously released under an open source license and then -modified by CFPB staff or its contractors is considered a "joint work" -(see 17 USC § 101); it is partially copyrighted, partially public domain, -and as a whole is protected by the copyrights of the non-government authors and -must be released according to the terms of the original open-source license. -Segments written by CFPB staff, and by contractors who are developing software -on behalf of CFPB are also in the public domain, and copyright and related -rights for that work are waived through the CC0 1.0 Universal dedication. - -For further details, please see the CFPB [Source Code Policy][policy]. - - -## CC0 1.0 Universal Summary - -This is a human-readable summary of the [Legal Code (read the full text)][CC0]. - -### No Copyright - -The person who associated a work with this deed has dedicated the work to -the public domain by waiving all of his or her rights to the work worldwide -under copyright law, including all related and neighboring rights, to the -extent allowed by law. - -You can copy, modify, distribute and perform the work, even for commercial -purposes, all without asking permission. See Other Information below. - -### Other Information - -In no way are the patent or trademark rights of any person affected by CC0, -nor are the rights that other persons may have in the work or in how the -work is used, such as publicity or privacy rights. - -Unless expressly stated otherwise, the person who associated a work with -this deed makes no warranties about the work, and disclaims liability for -all uses of the work, to the fullest extent permitted by applicable law. -When using or citing the work, you should not imply endorsement by the -author or the affirmer. - -[policy]: https://github.com/cfpb/source-code-policy/ -[CC0]: http://creativecommons.org/publicdomain/zero/1.0/legalcode - - -## Exceptions - -_Source code or other assets that are excluded from the TERMS should be listed -here. These may include dependencies that may be licensed differently or are -not in the public domain._ diff --git a/TERMS.rst b/TERMS.rst new file mode 100644 index 0000000..35488a9 --- /dev/null +++ b/TERMS.rst @@ -0,0 +1,56 @@ +As a work of the United States Government, this package (excluding any +exceptions listed below) is in the public domain within the United +States. Additionally, we waive copyright and related rights in the work +worldwide through the `CC0 1.0 Universal public domain +dedication `__. + +Software source code previously released under an open source license +and then modified by CFPB staff or its contractors is considered a +"joint work" (see 17 USC § 101); it is partially copyrighted, partially +public domain, and as a whole is protected by the copyrights of the +non-government authors and must be released according to the terms of +the original open-source license. Segments written by CFPB staff, and by +contractors who are developing software on behalf of CFPB are also in +the public domain, and copyright and related rights for that work are +waived through the CC0 1.0 Universal dedication. + +For further details, please see the CFPB `Source Code +Policy `__. + +CC0 1.0 Universal Summary +------------------------- + +This is a human-readable summary of the `Legal Code (read the full +text) `__. + +No Copyright +~~~~~~~~~~~~ + +The person who associated a work with this deed has dedicated the work +to the public domain by waiving all of his or her rights to the work +worldwide under copyright law, including all related and neighboring +rights, to the extent allowed by law. + +You can copy, modify, distribute and perform the work, even for +commercial purposes, all without asking permission. See Other +Information below. + +Other Information +~~~~~~~~~~~~~~~~~ + +In no way are the patent or trademark rights of any person affected by +CC0, nor are the rights that other persons may have in the work or in +how the work is used, such as publicity or privacy rights. + +Unless expressly stated otherwise, the person who associated a work with +this deed makes no warranties about the work, and disclaims liability +for all uses of the work, to the fullest extent permitted by applicable +law. When using or citing the work, you should not imply endorsement by +the author or the affirmer. + +Exceptions +---------- + +*Source code or other assets that are excluded from the TERMS should be +listed here. These may include dependencies that may be licensed +differently or are not in the public domain.* diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..6616593 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,191 @@ +# Makefile for Sphinx documentation + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext + +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 " singlehtml to make a single large HTML file" + @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 " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of 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." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +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/django-sheerlike.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-sheerlike.qhc" + +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/django-sheerlike" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-sheerlike" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +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." + +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..b2672ff --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,292 @@ +# -*- coding: utf-8 -*- +# +# django-sheerlike documentation build configuration file, created by +# sphinx-quickstart on Mon Jun 22 23:52:55 2015. +# +# 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 +import os +import shlex + +# 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.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'django-sheerlike' +copyright = u'2015, CFPB' +author = u'CFPB' + +# 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 = '1.0' +# The full version, including alpha/beta/rc tags. +release = '1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +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 patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_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 = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# 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 = [] + +# 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'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# 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_domain_indices = 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, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = 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 = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'django-sheerlikedoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'django-sheerlike.tex', u'django-sheerlike Documentation', + u'CFPB', '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 + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'django-sheerlike', u'django-sheerlike Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'django-sheerlike', u'django-sheerlike Documentation', + author, 'django-sheerlike', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..c8007d8 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +.. django-sheerlike documentation master file, created by + sphinx-quickstart on Mon Jun 22 23:52:55 2015. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to django-sheerlike's documentation! +============================================ + +Contents: + +.. toctree:: + :maxdepth: 2 + +.. automodule:: sheerlike + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..ebd3de2 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,263 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :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. singlehtml to make a single large HTML file + 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. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 2> nul +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-sheerlike.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-sheerlike.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/migrations/__init__.py b/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/opensource-checklist.md b/opensource-checklist.md deleted file mode 100644 index c4dca71..0000000 --- a/opensource-checklist.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -layout: base -title: "Open Source Checklist" ---- - -# Open Source Check List - -Prior to releasing a project to GitHub.com, walk through these items and ensure they are addressed. - -- **Has PII been removed?** - - Use [Clouseau](https://github.com/virtix/clouseau) for scanning source code. - - For an Open Source Release, attach the Clouseau output. - - If there are images, visually inspect each image to ensure there is no CFPB-specific information. - -- **Have security vulnerabilities been remediated?** - - Use the [OWASP Top 10](https://www.owasp.org/index.php/Top_10_2013) - - [National Vulnerability Database](http://nvd.nist.gov/) - - [SANS Swat Checklist](http://www.securingthehuman.org/developer/swat) - -- **Are we including any other open source products? If so, is there any conflict with our public domain release?** - -- **Is our `TERMS.md` included?** - -- **Is a `CHANGELOG.md` present and does it contain structured, consistently formatted recent history?** - - See and - - Some Inspiration: - -- **Are instructions for contributing included (`CONTRIBUTING.md`)?** - -- **Are installation instructions clearly written in the `README` _and_ tested on a clean machine?** - -- **Are all dependencies described in the `README`, `requirements.txt`, and/or `buildout.cfg`?** - -- **Are the API docs generated?** - -- **Are there unit tests?** - -- **If appplicable and possible, is it set up in TravisCI?** - -- **Have multiple people reviewed the code?** - -- **Is there a screenshot in the `README`, if applicable?** - - -## Copy this version to paste into a GitHub issue with live checkboxes: - -~~~ -- [ ] **Has PII been removed?** - - Use [Clouseau](https://github.com/virtix/clouseau) for scanning source code. - - If there are images, visually inspect each image to ensure there is no CFPB-specific information. -- [ ] **Have security vulnerabilities been remediated?** -- [ ] **Are we including any other open source products? If so, is there any conflict with our public domain release?** -- [ ] **Is our `TERMS.md` included?** -- [ ] **Is a `CHANGELOG.md` present and does it contain structured, consistently formatted recent history?** -- [ ] **Are instructions for contributing included (`CONTRIBUTING.md`)?** -- [ ] **Are installation instructions clearly written in the `README` _and_ tested on a clean machine?** -- [ ] **Are all dependencies described in the `README`, `requirements.txt`, and/or `buildout.cfg`?** -- [ ] **Are the API docs generated?** -- [ ] **Are there unit tests?** -- [ ] **If applicable and possible, is it set up in TravisCI?** -- [ ] **Have multiple people reviewed the code?** -- [ ] **Is there a screenshot in the `README`, if applicable?** -~~~ - ----- - - -## Take a look at the following projects as good models to follow: - - - [https://github.com/cfpb/qu](https://github.com/cfpb/qu) - - [https://github.com/cfpb/idea-box](https://github.com/cfpb/idea-box) - - [https://github.com/cfpb/hmda-tool](https://github.com/cfpb/hmda-tools) - - [https://github.com/cfpb/django-cache-tools](https://github.com/cfpb/django-cache-tools) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ba44c61 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +unipath>=1.1,<=2.0 +elasticsearch==1.6.0 +Jinja2==2.7.3 +python-dateutil==2.4.2 +pytz diff --git a/runtests.py b/runtests.py new file mode 100644 index 0000000..55ffa11 --- /dev/null +++ b/runtests.py @@ -0,0 +1,23 @@ +import os +import os.path +import sys + +import django +from django.core.management import call_command + +def run(): + this_dir = os.getcwd() + testproj_dir = os.path.join(this_dir, "test_project") + os.chdir(testproj_dir) + sys.path.append(testproj_dir) + os.environ["DJANGO_SETTINGS_MODULE"] = os.environ.get( + "DJANGO_SETTINGS_MODULE", "test_project.settings") + settings_file = os.environ["DJANGO_SETTINGS_MODULE"] + django.setup() + os.chdir(os.path.join(this_dir, 'sheerlike')) + call_command('test') + os.chdir(this_dir) + + +if __name__ == '__main__': + run() diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index fc37494..0000000 Binary files a/screenshot.png and /dev/null differ diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3dbe540 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,13 @@ +[metadata] +name = django-sheerlike +author = CFPB +author-email = tech@cfpb.gov +summary = OpenStack's setup automation in a reusable form +description-file = README.rst +license = Public Domain (CC0) +keywords = + setup + distutils +[files] +packages = + sheerlike diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..aa2d8a0 --- /dev/null +++ b/setup.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +from setuptools import setup + +setup( + setup_requires=['pbr'], + pbr=True, +) diff --git a/sheerlike/__init__.py b/sheerlike/__init__.py new file mode 100644 index 0000000..9827606 --- /dev/null +++ b/sheerlike/__init__.py @@ -0,0 +1,95 @@ +from __future__ import absolute_import # Python 2 only + +import os +import os.path +import functools +import warnings + +from django.contrib.staticfiles.storage import staticfiles_storage +from django.core.urlresolvers import reverse +from django.conf import settings + +from jinja2 import Environment +import jinja2.runtime +from jinja2.runtime import Context + +from unipath import Path + +from .query import QueryFinder, more_like_this, get_document +from .filters import selected_filters_for_field, is_filter_selected +from .templates import date_formatter +from .middleware import get_request + +PERMALINK_REGISTRY={} + +def register_permalink(sheer_type, url_pattern_name): + PERMALINK_REGISTRY[sheer_type]=url_pattern_name + +def url_for(app, filename): + if app == 'static': + return staticfiles_storage.url(filename) + else: + raise ValueError("url_for doesn't know about %s" % app) + +def date_filter(value, format="%Y-%m-%d", tz="America/New_York"): + return date_formatter(value, format, tz) + + +class SheerlikeContext(Context): + def __init__(self, environment, parent, name, blocks): + super(SheerlikeContext, self).__init__(environment, parent, name, blocks) + self.vars['request'] = get_request() + +# Monkey patch not needed in master version of Jinja2 +# https://github.com/mitsuhiko/jinja2/commit/f22fdd5ffe81aab743f78290071b0aa506705533 +jinja2.runtime.Context = SheerlikeContext + +class SheerlikeEnvironment(Environment): + def join_path(self, template, parent): + dirname = os.path.dirname(parent) + segments = dirname.split('/') + paths = [] + collected = '' + for segment in segments: + collected += segment + '/' + paths.insert(0,collected[:]) + for p in paths: + relativepath = os.path.join(p, template) + for search in self.loader.searchpath: + filesystem_path = os.path.join(search, relativepath) + if os.path.exists(filesystem_path): + return relativepath + return template + +def environment(**options): + queryfinder = QueryFinder() + + searchpath =[] + staticdirs = [] + + sites = settings.SHEER_SITES + for site in sites: + site_path = Path(site) + searchpath.append(site_path) + searchpath.append(site_path.child('_includes')) + searchpath.append(site_path.child('_layouts')) + staticdirs.append(site_path.child('static')) + + options['loader'].searchpath += searchpath + settings.STATICFILES_DIRS = staticdirs + + env = SheerlikeEnvironment(**options) + env.globals.update({ + 'static': staticfiles_storage.url, + 'url_for':url_for, + 'url': reverse, + 'queries': queryfinder, + 'more_like_this': more_like_this, + 'get_document': get_document, + 'selected_filters_for_field': selected_filters_for_field, + 'is_filter_selected': is_filter_selected, + }) + env.filters.update({ + 'date':date_filter + }) + return env diff --git a/sheerlike/admin.py b/sheerlike/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/sheerlike/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/sheerlike/filters.py b/sheerlike/filters.py new file mode 100644 index 0000000..70003e2 --- /dev/null +++ b/sheerlike/filters.py @@ -0,0 +1,107 @@ +import re +import calendar +import datetime +from dateutil.parser import parse + +from .middleware import get_request + +def generate_term_filters(multidict, filter_keys): + ''' + This generates the ElasticSearch filter DSL for filters that check whether + a field matches a certain string. Groupings of the same exact filter, such + as filter_fieldname, are combined into an OR filter, while the major + groupings of separate filters are all combined into an AND filter. + ''' + term_main = {"and": []} + for key in filter_keys: + field = key.replace('filter_', '') + filter_type_main = {"or": []} + values = multidict.getlist(key) + #from nose.tools import set_trace; set_trace(); + for val in values: + term_single = {"term": {}} + term_single["term"][field] = val + filter_type_main["or"].append(term_single) + term_main["and"].append(filter_type_main) + return term_main + +def generate_range_filters(multidict, filter_keys): + ''' + This generates the ElasticSearch filter DSL for filters that check whether + a field is within a certain range + ''' + range_clause = {"range": {}} + for key in filter_keys: + full_field = key.replace('filter_range_', '') + # We account for potential underscores in the field name itself + # e.g. comment_count + operator = full_field[full_field.rfind('_') + 1:] + field = full_field[:full_field.rfind('_')] + if field not in range_clause["range"]: + range_clause["range"][field] = {} + # If there are multiples of the same date filter, this will take + # the first + + # The django version of MultiDict returns the actual object + # if there is only one, and a list otherwise + value = multidict.get(key) + range_clause["range"][field][operator] = value + + # Validate date range input + + # First check if both date_lte and date_gte are present + # If the 'start' date is after the 'end' date, swap them + if 'date' in range_clause['range']: + if all(x in range_clause['range']['date'] for x in ('lte', 'gte')) and \ + parse(range_clause['range']['date']['gte'], default=datetime.date.today().replace(day=1)) > \ + parse(range_clause['range']['date']['lte'], default=datetime.date.today().replace(day=1)): + range_clause['range']['date']['gte'], range_clause['range']['date']['lte'] = \ + range_clause['range']['date'][ + 'lte'], range_clause['range']['date']['gte'] + # If either date matches the YYYY-M[M] format, append the + # appropriate day + if 'lte' in range_clause['range']['date'] and \ + re.compile("^[0-9]{4}-[0-9]{1,2}$").match(range_clause['range']['date']['lte']): + year, month = range_clause['range']['date']['lte'].split('-') + last_day_of_month = calendar.monthrange( + int(year), int(month))[1] + range_clause['range']['date'][ + 'lte'] += "-{0}".format(last_day_of_month) + if 'gte' in range_clause['range']['date'] and \ + re.compile("^[0-9]{4}-[0-9]{1,2}$").match(range_clause['range']['date']['gte']): + range_clause['range']['date']['gte'] += "-1" + return range_clause + + + +def filter_dsl_from_multidict(multidict): + # Split the filters between 'range' and 'term', making sure the query + # value isn't blank + term_filter_keys = [r for r in [k for k in multidict.keys() if re.compile("^filter_(?!range_)").match(k)] + if multidict[r]] + range_filter_keys = [r for r in [k for k in multidict.keys() if re.compile("^filter_range_").match(k)] + if multidict[r]] + final_filters = [] + if term_filter_keys: + term_clause = generate_term_filters(multidict, term_filter_keys) + final_filters.append(term_clause) + + if range_filter_keys: + range_clause = generate_range_filters(multidict, range_filter_keys) + final_filters.append(range_clause) + return final_filters + + +def selected_filters_from_multidict(multidict, field): + return [k for k in multidict.getlist('filter_' + field) if k] + + +def selected_filters_for_field(fieldname): + multidict = get_request().GET + return selected_filters_from_multidict(multidict, fieldname) + + +def is_filter_selected(fieldname, value): + multidict = get_request().GET + return value in selected_filters_from_multidict(multidict, fieldname) + diff --git a/sheerlike/management/__init__.py b/sheerlike/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sheerlike/management/commands/__init__.py b/sheerlike/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sheerlike/management/commands/runindexer.py b/sheerlike/management/commands/runindexer.py new file mode 100644 index 0000000..4b74387 --- /dev/null +++ b/sheerlike/management/commands/runindexer.py @@ -0,0 +1,49 @@ +import sys +import os.path +import codecs +import json + +from importlib import import_module + +from django.core.management.base import BaseCommand, CommandError +from django.conf import settings + +from unipath import Path + +class Command(BaseCommand): + help = "populate a Django model using a Sheer indexer" + + + def add_arguments(self,parser): + parser.add_argument('indexer_name') + parser.add_argument('target_model') + + def handle(self, *args, **options): + indexer_name = options ['indexer_name'] + + sheer_sites = settings.SHEER_SITES + sheer_libs = [Path(s).child('_lib') for s in sheer_sites] + sys.path += sheer_libs + + processors = {} + + possible_processor_configs =\ + [Path(s).child('_settings').child('processors.json') for s in sheer_sites] + + for procjson in possible_processor_configs: + if os.path.exists(procjson): + with codecs.open(procjson, encoding='utf8') as jsonfile: + raw_json = jsonfile.read() + merged_json = os.path.expandvars(raw_json) + config = json.loads(merged_json) + processors.update(config) + + if indexer_name in processors: + mod = import_module(processors[indexer_name]['processor']) + generator = mod.documents(indexer_name, **processors[indexer_name]) + + for doc in generator: + self.stdout.write(str(doc)) + + else: + raise CommandError('could not find a processor for %s' % indexer_name) diff --git a/sheerlike/middleware.py b/sheerlike/middleware.py new file mode 100644 index 0000000..15fc1c9 --- /dev/null +++ b/sheerlike/middleware.py @@ -0,0 +1,31 @@ +from threading import local +from django.http import HttpResponse +from django.core.urlresolvers import resolve + +_active = local() + +def get_request(): + return _active.request + +class FlaskyHeaderGetter(object): + def __init__(self, request): + self.request= request + + def __getitem__(self, key): + django_key = 'HTTP_' + key.upper().replace('-','_') + return self.request.META.get(django_key) + + def get(self, key): + return self.__getitem__(key) + + + +class GlobalRequestMiddleware(object): + def process_view(self, request, view_func, view_args, view_kwargs): + _active.request = request + request.headers = FlaskyHeaderGetter(request) + request.url = "%s://%s%s" % (request.scheme, request.get_host(), + request.get_full_path()) + request.url_rule = request.resolver_match + request.url_rule.endpoint = request.resolver_match.url_name + return None diff --git a/sheerlike/models.py b/sheerlike/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/sheerlike/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/sheerlike/query.py b/sheerlike/query.py new file mode 100644 index 0000000..8d6ca49 --- /dev/null +++ b/sheerlike/query.py @@ -0,0 +1,319 @@ +import os +import codecs +import logging +import json + +from collections import namedtuple + +from django.utils.datastructures import MultiValueDict as MultiDict +from django.conf import settings +from django.utils.http import urlencode +from django.core.urlresolvers import reverse + +import dateutil.parser + +from time import mktime, strptime +import datetime + +import elasticsearch + +from unipath import Path + +#from sheer.utility import find_in_search_path +from .filters import filter_dsl_from_multidict + +from .middleware import get_request + + +ALLOWED_SEARCH_PARAMS = ('doc_type', + 'analyze_wildcard', 'analyzer', 'default_operator', 'df', + 'explain', 'fields', 'indices_boost', 'lenient', + 'allow_no_indices', 'expand_wildcards', 'ignore_unavailable', + 'lowercase_expanded_terms', 'from_', 'preference', 'q', 'routing', + 'scroll', 'search_type', 'size', 'sort', 'source', 'stats', + 'suggest_field', 'suggest_mode', 'suggest_size', 'suggest_text', 'timeout', + 'version') + + +FakeQuery = namedtuple('FakeQuery',['es','es_index']) + + + +def mapping_for_type(typename, es, es_index): + + return es.indices.get_mapping(index=es_index, doc_type=typename) + + +def field_or_source_value(fieldname, hit_dict): + if 'fields' in hit_dict and fieldname in hit_dict['fields']: + return hit_dict['fields'][fieldname] + + if '_source' in hit_dict and fieldname in hit_dict['_source']: + return hit_dict['_source'][fieldname] + + +def datatype_for_fieldname_in_mapping(fieldname, hit_type, mapping_dict, es, es_index): + + try: + return mapping_dict[es_index]["mappings"][hit_type]["properties"][fieldname]["type"] + except KeyError: + return None + + +def coerced_value(value, datatype): + if datatype == None or value == None: + return value + + TYPE_MAP = {'string': unicode, + 'date': dateutil.parser.parse, + 'dict': dict, + 'float': float, + 'long': float, + 'boolean': bool} + + coercer = TYPE_MAP[datatype] + + if type(value) == list: + if value and type(value[0]) == list: + return [[coercer(y) for y in v] for v in value] + else: + return [coercer(v) for v in value] or "" + else: + return coercer(value) + + +class QueryHit(object): + + def __init__(self, hit_dict, es, es_index): + self.hit_dict = hit_dict + self.type = hit_dict['_type'] + self.es = es + self.es_index = es_index + self.mapping = mapping_for_type(self.type, es=es, es_index=es_index) + + def __str__(self): + return str(self.hit_dict.get('_source')) + + def __repr__(self): + return self.__str__() + + @property + def permalink(self): + import sheerlike + if self.type in sheerlike.PERMALINK_REGISTRY: + pattern_name = sheerlike.PERMALINK_REGISTRY[self.type] + return reverse(pattern_name,kwargs=dict(doc_id=self._id)) + else: + raise NotImplementedError("Please use django's reverse url system," + "or register a permalink for %s" % self.type) + + def __getattr__(self, attrname): + value = field_or_source_value(attrname, self.hit_dict) + datatype = datatype_for_fieldname_in_mapping( + attrname, self.type, self.mapping, self.es, self.es_index) + return coerced_value(value, datatype) + + def json_compatible(self): + hit_dict = self.hit_dict + fields = hit_dict.get('fields') or hit_dict.get('_source', {}).keys() + return dict((field, getattr(self, field)) for field in fields) + + +class QueryResults(object): + + def __init__(self, query, result_dict, pagenum=1): + self.result_dict = result_dict + self.total = int(result_dict['hits']['total']) + self.query = query + + # confusing: using the word 'query' to mean different things + # above, it's the Query object + # below, it's Elasticsearch query DSL + + if 'query' in result_dict: + self.size = int(result_dict['query'].get('size', '10')) + self.from_ = int(result_dict['query'].get('from', 1)) + self.pages = self.total / self.size + \ + int(self.total % self.size > 0) + else: + self.size, self.from_, self.pages = 10, 1, 1 + + self.current_page = pagenum + + def __iter__(self): + if 'hits' in self.result_dict and 'hits' in self.result_dict['hits']: + for hit in self.result_dict['hits']['hits']: + query_hit = QueryHit(hit, self.query.es, self.query.es_index) + yield query_hit + + def aggregations(self, fieldname): + if "aggregations" in self.result_dict and \ + fieldname in self.result_dict['aggregations']: + return self.result_dict['aggregations'][fieldname]['buckets'] + + def json_compatible(self): + response_data = {} + response_data['total'] = self.result_dict['hits']['total'] + if self.size: + response_data['size'] = self.size + + if self.from_: + response_data['from'] = self.from_ + + if self.pages: + response_data['pages'] = self.pages + response_data['results'] = [ + hit.json_compatible() for hit in self.__iter__()] + return response_data + + def url_for_page(self, pagenum): + request = get_request() + current_args = request.GET + args_dict = MultiDict(current_args) + if pagenum != 1: + args_dict['page'] = pagenum + elif 'page' in args_dict: + del args_dict['page'] + + encoded = urlencode(args_dict, doseq=True) + if encoded: + url = "".join([request.path, "?", urlencode(args_dict, doseq=True)]) + return url + else: + return request.path + + +class Query(object): + + def __init__(self, filename,es, es_index, json_safe=False): + # TODO: make the no filename case work + + self.es_index = es_index + self.es = es + self.filename = filename + self.__results = None + self.json_safe = json_safe + + def search(self, aggregations=None, use_url_arguments=True, size=10, **kwargs): + query_file = json.loads(file(self.filename).read()) + query_dict = query_file['query'] + + ''' + These dict constructors split the kwargs from the template into filter + arguments and arguments that can be placed directly into the query body. + The dict constructor syntax supports python 2.6, 2.7, and 3.x + If python 2.7, use dict comprehension and iteritems() + With python 3, use dict comprehension and items() (items() replaces + iteritems and is just as fast) + ''' + filter_args = dict((key, value) for (key, value) in kwargs.items() + if key.startswith('filter_')) + non_filter_args = dict((key, value) for (key, value) in kwargs.items() + if not key.startswith('filter_')) + query_dict.update(non_filter_args) + pagenum = 1 + + request = get_request() + # Add in filters from the template. + new_multidict = MultiDict() + # First add the url arguments if requested + if use_url_arguments: + new_multidict = MultiDict(request.GET.copy()) + # Next add the arguments from the search() function used in the + # template + for key, value in filter_args.items(): + new_multidict.update({key: value}) + + filters = filter_dsl_from_multidict(new_multidict) + args_flat = request.GET.copy() + query_body = {} + + if aggregations: + aggs_dsl = {} + if type(aggregations) is str: + aggregations = [aggregations] # so we can treat it as a list + for fieldname in aggregations: + aggs_dsl[fieldname] = {'terms': + {'field': fieldname, 'size': 10000}} + query_body['aggs'] = aggs_dsl + else: + if 'page' in args_flat: + args_flat['from_'] = int( + query_dict.get('size', '10')) * (int(args_flat['page']) - 1) + pagenum = int(args_flat['page']) + + args_flat_filtered = dict( + [(k, v) for k, v in args_flat.items() if v]) + query_dict.update(args_flat_filtered) + query_body['query'] = {'filtered': {'filter': {}}} + if filters: + query_body['query']['filtered']['filter'][ + 'and'] = [f for f in filters] + + if 'filters' in query_file: + if 'and' not in query_body['query']['filtered']['filter']: + query_body['query']['filtered']['filter']['and'] = [] + for json_filter in query_file['filters']: + query_body['query']['filtered'][ + 'filter']['and'].append(json_filter) + final_query_dict = dict((k, v) + for (k, v) in query_dict.items() if k in ALLOWED_SEARCH_PARAMS) + final_query_dict['index'] = self.es_index + final_query_dict['body'] = query_body + response = self.es.search(**final_query_dict) + response['query'] = query_dict + return QueryResults(self,response, pagenum) + + def possible_values_for(self, field, **kwargs): + results = self.search(aggregations=[field], **kwargs) + return results.aggregations(field) + + + +class QueryFinder(object): + + def __init__(self): + self.es = elasticsearch.Elasticsearch(settings.SHEER_ELASTICSEARCH_SERVER) + self.es_index = settings.SHEER_ELASTICSEARCH_INDEX + self.searchpath = [Path(site).child('_queries') for site in settings.SHEER_SITES] + + def __getattr__(self, name): + for dir in self.searchpath: + query_filename = name + ".json" + query_file_path = os.path.join(dir, query_filename) + + if os.path.exists(query_file_path): + query = Query(query_file_path, self.es, self.es_index) + return query + + +class QueryJsonEncoder(json.JSONEncoder): + query_classes = [QueryResults, QueryHit] + + def default(self, obj): + if type(obj) in (datetime.datetime, datetime.date): + return obj.isoformat() + if type(obj) in self.query_classes: + return obj.json_compatible() + + return json.JSONEncoder.default(self, obj) + + +def more_like_this(hit, **kwargs): + es = elasticsearch.Elasticsearch(settings.SHEER_ELASTICSEARCH_SERVER) + es_index = settings.SHEER_ELASTICSEARCH_INDEX + doctype, docid = hit.type, hit._id + raw_results = es.mlt( + index=es_index, doc_type=doctype, id=docid, **kwargs) + + # this is bad and I should feel bad + # (I do) + fake_query = FakeQuery(es,es_index) + return QueryResults(fake_query,raw_results) + +def get_document(doctype, docid): + es = elasticsearch.Elasticsearch(settings.SHEER_ELASTICSEARCH_SERVER) + es_index = settings.SHEER_ELASTICSEARCH_INDEX + raw_results = es.get(index=es_index, doc_type=doctype, id=docid) + return QueryHit(raw_results, es, es_index) + diff --git a/sheerlike/templates.py b/sheerlike/templates.py new file mode 100644 index 0000000..bd62374 --- /dev/null +++ b/sheerlike/templates.py @@ -0,0 +1,13 @@ +import datetime +from dateutil import parser +from pytz import timezone + +def date_formatter(value, format="%Y-%m-%d", tz='America/New_York'): + if type(value) not in [datetime.datetime, datetime.date]: + date = parser.parse(value, default=datetime.datetime.today().replace(day=1)) + naive = date.replace(tzinfo=None) + dt = timezone(tz).localize(naive) + else: + dt = value + + return dt.strftime(format) diff --git a/sheerlike/test_filters.py b/sheerlike/test_filters.py new file mode 100644 index 0000000..42e711b --- /dev/null +++ b/sheerlike/test_filters.py @@ -0,0 +1,77 @@ +from django.utils.datastructures import MultiValueDict as MultiDict + +from sheerlike import filters + + +class TestArgParsing(object): + + def setup(self): + self.args = MultiDict([('filter_category', ['cats', 'dogs']), + ('filter_planet', ['earth']), + ('filter_range_date_lte', ['2014-6-1']), + ('filter_range_comment_count_gt', ['100'])]) + + def test_args_to_filter_dsl(self): + filter_dsl = filters.filter_dsl_from_multidict(self.args) + # the existing tests here seemed to depend of the other + # of dictionary keys, which is undefined + + def test_range_args(self): + filter_dsl = filters.filter_dsl_from_multidict(self.args) + assert('range' in filter_dsl[1]) + assert('date' in filter_dsl[1]['range']) + assert('comment_count' in filter_dsl[1]['range']) + assert('2014-6-1' == filter_dsl[1]['range']['date']['lte']) + assert('100' == filter_dsl[1]['range']['comment_count']['gt']) + + def test_filters_for_field(self): + selected = filters.selected_filters_from_multidict( + self.args, 'category') + assert (('cats') in selected) + assert (('dogs') in selected) + + +class TestDateValidation(object): + + def test_date_validation_incorrect_range(self): + args = MultiDict([('filter_range_date_gte', ['2014-6']), + ('filter_range_date_lte', ['2013-6'])]) + filter_dsl = filters.filter_dsl_from_multidict(args) + assert(filter_dsl[0]['range']['date']['gte'] == '2013-6-1') + assert(filter_dsl[0]['range']['date']['lte'] == '2014-6-30') + + def test_date_validation_correct_range(self): + args = MultiDict([('filter_range_date_gte', ['2013-6']), + ('filter_range_date_lte', ['2014-6'])]) + filter_dsl = filters.filter_dsl_from_multidict(args) + assert(filter_dsl[0]['range']['date']['gte'] == '2013-6-1') + assert(filter_dsl[0]['range']['date']['lte'] == '2014-6-30') + + def test_date_validation_with_days_correct_range(self): + args = MultiDict([('filter_range_date_gte', ['2014-1-23']), + ('filter_range_date_lte', ['2014-6-23'])]) + filter_dsl = filters.filter_dsl_from_multidict(args) + assert(filter_dsl[0]['range']['date']['gte'] == '2014-1-23') + assert(filter_dsl[0]['range']['date']['lte'] == '2014-6-23') + + def test_date_validation_with_days_incorrect_range(self): + args = MultiDict([('filter_range_date_gte', ['2014-6-23']), + ('filter_range_date_lte', ['2014-1-23'])]) + filter_dsl = filters.filter_dsl_from_multidict(args) + assert(filter_dsl[0]['range']['date']['gte'] == '2014-1-23') + assert(filter_dsl[0]['range']['date']['lte'] == '2014-6-23') + + def test_default_days_correct_range(self): + args = MultiDict([('filter_range_date_gte', ['2014-1']), + ('filter_range_date_lte', ['2014-6'])]) + filter_dsl = filters.filter_dsl_from_multidict(args) + assert(filter_dsl[0]['range']['date']['gte'] == '2014-1-1') + assert(filter_dsl[0]['range']['date']['lte'] == '2014-6-30') + + def test_default_days_incorrect_range(self): + args = MultiDict([('filter_range_date_gte', ['2014-6']), + ('filter_range_date_lte', ['2014-1'])]) + filter_dsl = filters.filter_dsl_from_multidict(args) + #from nose.tools import set_trace;set_trace() + assert(filter_dsl[0]['range']['date']['gte'] == '2014-1-1') + assert(filter_dsl[0]['range']['date']['lte'] == '2014-6-30') diff --git a/sheerlike/views/__init__.py b/sheerlike/views/__init__.py new file mode 100644 index 0000000..7d8431d --- /dev/null +++ b/sheerlike/views/__init__.py @@ -0,0 +1,9 @@ +from django.shortcuts import render + +# Create your views here. + +def server_error(request, template_name='404.html'): + from django.template import RequestContext + from django.http import HttpResponseServerError + t = get_template(template_name) + return HttpResponseServerError(t.render(RequestContext(request))) diff --git a/sheerlike/views/generic.py b/sheerlike/views/generic.py new file mode 100644 index 0000000..92835ee --- /dev/null +++ b/sheerlike/views/generic.py @@ -0,0 +1,57 @@ +from django.views.generic.base import TemplateView +from django.http import Http404 +from django.template import TemplateDoesNotExist + +from elasticsearch import TransportError + +from sheerlike.query import get_document + +class SheerTemplateView(TemplateView): + doc_type = None + local_name = 'object' + default_template = None + + def get_template_names(self,*args, **kwargs): + if self.template_name: + return [self.template_name] + + request = self.request + templates = [] + + if request.path.endswith('/'): + templates.append(request.path[1:]+'index.html') + else: + templates.append(request.path[1:]) + if 'doc_id' in self.kwargs and self.default_template: + templates.append(self.default_template) + + return templates + + def get_context_data(self, **kwargs): + context = super(SheerTemplateView, self).get_context_data(**self.kwargs) + if 'doc_id' in kwargs: + doc_id = kwargs.pop('doc_id') + self.doc_id = doc_id + try: + document = get_document(doctype=self.doc_type, + docid=doc_id) + context[self.local_name] = document + + return context + except TransportError: + pass + + return context + + def render_to_response(self, context, **response_kwargs): + response = super(SheerTemplateView,self).render_to_response(context, **response_kwargs) + try: + template = response.resolve_template(response.template_name) + + except TemplateDoesNotExist: + raise Http404("could not find template in " + str(response.template_name)) + + if template.template.name == self.default_template and self.local_name not in context: + raise Http404('fell back to %s, but %s with id %s not found' % + (self.default_template, self.doc_type, self.doc_id)) + return response diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..5f2ea3d --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +-r requirements.txt +django-nose diff --git a/test_project/manage.py b/test_project/manage.py new file mode 100755 index 0000000..0fc36a3 --- /dev/null +++ b/test_project/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/test_project/test_project/__init__.py b/test_project/test_project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py new file mode 100644 index 0000000..01367ca --- /dev/null +++ b/test_project/test_project/settings.py @@ -0,0 +1,108 @@ +""" +Django settings for test_project project. + +Generated by 'django-admin startproject' using Django 1.8.2. + +For more information on this file, see +https://docs.djangoproject.com/en/1.8/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.8/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '(sumlf2_5_f54v-x94!)9ag4bmt-uf6n96m_5#nj%7qas=n7++' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'sheerlike', + 'django_nose', +) + +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.security.SecurityMiddleware', +) + +ROOT_URLCONF = 'test_project.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'test_project.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.8/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Internationalization +# https://docs.djangoproject.com/en/1.8/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.8/howto/static-files/ + +STATIC_URL = '/static/' + +# Set test runner +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +NOSE_ARGS = ['--testmatch=(?:^|[\\b_\\.%s-])[Tt]est|^test_', '-s'] diff --git a/test_project/test_project/urls.py b/test_project/test_project/urls.py new file mode 100644 index 0000000..7dc47ea --- /dev/null +++ b/test_project/test_project/urls.py @@ -0,0 +1,21 @@ +"""test_project URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.8/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Add an import: from blog import urls as blog_urls + 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) +""" +from django.conf.urls import include, url +from django.contrib import admin + +urlpatterns = [ + url(r'^admin/', include(admin.site.urls)), +] diff --git a/test_project/test_project/wsgi.py b/test_project/test_project/wsgi.py new file mode 100644 index 0000000..cb26c81 --- /dev/null +++ b/test_project/test_project/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for test_project project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") + +application = get_wsgi_application() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..bba48d0 --- /dev/null +++ b/tox.ini @@ -0,0 +1,6 @@ +[tox] +envlist=py27,py33,py34 + +[testenv] +deps=-rtest-requirements.txt +commands=python runtests.py