pax_global_header00006660000000000000000000000064136220762510014516gustar00rootroot0000000000000052 comment=f22d9c53bbde22e924d9ccf1f4bd5a263a45df1f django-waffle-0.20.0/000077500000000000000000000000001362207625100143015ustar00rootroot00000000000000django-waffle-0.20.0/.github/000077500000000000000000000000001362207625100156415ustar00rootroot00000000000000django-waffle-0.20.0/.github/stale.yml000066400000000000000000000007621362207625100175010ustar00rootroot00000000000000daysUntilStale: 30 daysUntilClose: 14 exemptLabels: - security - pinned - pr-wanted staleLabel: stale markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. closeComment: > This issue has been closed as stale because it has not had any recent activity. Please feel free to re-open if the issue is still relevant. Thank you for your contributions! django-waffle-0.20.0/.gitignore000066400000000000000000000002511362207625100162670ustar00rootroot00000000000000*.py[oc] *.swp build dist *.egg-info *test.db .coverage .idea/ *.iml .project .pydevproject .settings .tox/ .DS_Store docs/_* # Visual Studio Code config files .vscode django-waffle-0.20.0/.travis.yml000066400000000000000000000024521362207625100164150ustar00rootroot00000000000000language: python sudo: false dist: bionic matrix: include: - { python: "3.5", env: TOXENV=py35-django111 } - { python: "3.6", env: TOXENV=py36-django111 } - { python: "3.6", env: TOXENV=py36-django22 } - { python: "3.6", env: TOXENV=py36-django30 } - { python: "3.7", env: TOXENV=py37-django22 } - { python: "3.7", env: TOXENV=py37-django30 } - { python: "3.8", env: TOXENV=py38-django22 } - { python: "3.8", env: TOXENV=py38-django30 } - { python: "3.8", env: TOXENV=i18n } install: - pip install tox tox-travis # Some tests may require accessing the database from multiple threads # (see #296), which isn't supported by the sqlite3 engine. Run the tests with # Postgres in CI to allow full test coverage. services: postgresql addons: postgresql: "9.6" before_script: - psql -c 'create database waffle_test;' -U postgres env: - DATABASE_URL=postgres://postgres@localhost:5432/waffle_test script: - tox deploy: provider: pypi user: django-waffle password: secure: sQzN+jn7JdkIJbXsjS1uRueNVo2AsPh2iJChDAisWu8qFRMFREbHCKRu33+WZ9Vbw0NuRC2Wl2DknPTKYBtMq+4YzbKyRLAXvf/qAzWwcbjQsPAz1bQnfDBbOhcewiltfVf3AEbsq97NdKhOS4+T0R3/eqMhbAM9PVP2WO78cvA= distributions: sdist bdist_wheel on: tags: true python: 3.7 env: TOXENV=i18n django-waffle-0.20.0/.tx/000077500000000000000000000000001362207625100150125ustar00rootroot00000000000000django-waffle-0.20.0/.tx/config000066400000000000000000000003401362207625100161770ustar00rootroot00000000000000[main] host = https://www.transifex.com [django-waffle.django-po] file_filter = waffle/locale//LC_MESSAGES/django.po minimum_perc = 0 source_file = waffle/locale/en_US/LC_MESSAGES/django.po source_lang = en type = PO django-waffle-0.20.0/CHANGES000066400000000000000000000122531362207625100152770ustar00rootroot00000000000000================ Waffle Changelog ================ v0.20.0 ======= - Dropped support for Python 2.7 - Removed deprecation warnings for ugettext. v0.19.0 ======= - Dropped support for Django 2.0, 2.1, and Python 3.4. - Made tests for Jinja2 optional while waiting for django-jinja to be compatible with Django 3.0. - Add support for Django 3.0 by removing use of deprecated functionality from Django 2.2. v0.18.0 ======= - Updated `MIDDLEWARE` setting name for Django 1.10+ - Improved cache performance for `is_active_for_user` - Corrected log formatting - Added log entries for admin quick link actions - Added permissions for admin actions v0.17.0 ======= - Fixed documentation issues - Added class-based view mixins v0.16.0 ======= - Added support for Django 2.1 and 2.2 - Flushing cache after modifying flags' groups and users - Removed redundant log line - Corrected version in `waffle/__init__.py` - Fixed bug in tests - Using strings as cache keys instead of bytes - Passing effects of test decorator to child classes -- NOTE: This introduced a backwards-incompatible change for the testutils override decorators. See https://github.com/django-waffle/django-waffle/pull/331 for details. v0.15.1 ======= - Optionally logging missing flags, samples, and switches - Added --users option to waffle_flag management command - Updated testutils to flush caches - Improved admin site for users and groups - Fixed global cache thread safety issues v0.15.0 ======= - Dropped support for Django < 1.11 - Dropped support for Python 3.3 - Added settings `WAFFLE_CREATE_MISSING_(FLAG|SWITCHES|SAMPLES)` to optionally create missing objects in the database automatically - Allow serializing/de-serializing waffle models by natural key - Added pluggable Flag model v0.14.0 ======= - Added i18n support - Added Russian translations - Add management command to delete waffle data v0.13 ===== - Added support for Django 2.0. - Added support for search the name and notes fields in Django admin. - Fixed small bugs in management commands. - Fixed small documentation issues. - Updated `waffle_flag` management command to accept user groups. - Added setting `WAFFLE_READ_FROM_WRITE_DB` to read from write DB on cache miss. v0.12 ===== - Drop support for Django<1.8 and Python<2.7. - Moved bulk of code from waffle.*_is_active methods to .is_active instance methods. - Centralized caching code behind Class.get() methods. - Significant caching overhaul. - Automatically invalidate cache on waffle upgrade. v0.11.1 ======= - Fix Django 1.9 support. - Fix several Python >=3.4 issues. - Fix output of `waffle_switch --list`. - Fix small documentation issues. - Cache a plain list instead of a ValuesListQuerySet. v0.11 ===== - Support Django 1.8. - Move from jingo-specific to generic Jinja2 template support. - Added tools for integration testing. - Drop Django 1.5 support. - Fix several code and documentation bugs. - Add optional redirect parameter to view decorators. v0.10.2 ======= - Overhaul documentation. - Move CLI commands to waffle_(flag|sample|switch) to be more polite. - Add override_(flag|sample|switch) testing tools. - Changed the default of WAFFLE_SECURE to True. v0.10.1 ======= - Support Python 3. - Support Django 1.7. - Add WAFFLE_CACHE_NAME. - Fix caching for empty lists. v0.10.0 ======= - Replace waffle.get_flags with waffle.{FLAGS,SWITCHES,SAMPLES} in JS. - Update Custom User Models for Django 1.6 support. - Support WaffleJS inline in templates. - Improve test infrastructure and coverage. v0.9.2 ====== - Add get_flags method to waffle.js. - Fix issue with South migrations and custom user models in Django 1.5. - Document command-line access and get more useful information from it. - Support non-naive datetimes when appropriate. - Fix a cache invalidation issue. v0.9.1 ====== - Real Django 1.5 support. - JavaScript obeys WAFFLE_*_DEFAULT settings. v0.9 ==== - Reorganized documentation. - Hash form values for better memcached keys. - Simplified and improved Django template tags. - Renamed JS functions to *_is_active to avoid reserved keywords. v0.8.1 ====== - Fix cache flushing issues. - Fix order of flag_is_active checks. - Add a waffle.urls module. - Add management commands. - Add language support to flags. - Better caching for missing flags/switches/samples. - Re-add 'note' field. - Created a set_flag method to make custom flag cookie triggers easier. v0.8 ==== - Fix issue with repeated flag_is_active calls. - Add created/modified dates to models. - Add WAFFLE_CACHE_PREFIX settings. v0.7.6 ====== - Fix waffle template functions when no request is present. - Added testing mode to flags. - Add WAFFLE_*_DEFAULT for switches and samples. v0.7.5 ====== - Fix issue with stale cache using bulk admin actions. v0.7.4 ====== - Fix waffle.js in Safari. v0.7.2 ====== - Handle 404s correctly. v0.7.1 ====== - I am bad at packaging. v0.7 ==== - Add 'note' field. - Add migrations for Samples. - Clean up Jinja2 functions. v0.6 ==== - Add Samples. v0.5 ==== - Fix waffle.js view with Switches. - Add South migrations. - Cache values to save database queries. v0.4 ==== - Add Switches. v0.3 ==== - Add waffle.js view. v0.2.1 ====== - Add bulk admin actions. v0.2 ==== - Add rollout mode to Flags. django-waffle-0.20.0/CONTRIBUTING.rst000066400000000000000000000044001362207625100167400ustar00rootroot00000000000000.. _about-contributing: .. highlight:: shell ====================== Contributing to Waffle ====================== Waffle is pretty simple to hack, and has a decent test suite! Here's how to patch Waffle, add tests, run them, and contribute changes. **Please** `open a new issue`_ to discuss a new feature before beginning work on it. Not all suggestions are accepted. The :ref:`Goals ` may help guide which features are likely to be accepted. Set Up ====== Setting up an environment is easy! You'll want ``virtualenv`` and ``pip``, then just create a new virtual environment and install the requirements:: $ mkvirtualenv waffle $ pip install -r requirements.txt Done! Writing Patches =============== Fork_ Waffle and create a new branch off master for your patch. Run the tests often:: $ ./run.sh test Try to keep each branch to a single feature or bugfix. .. note:: To update branches, please **rebase** onto master, do not merge master into your branch. Submitting Patches ================== Open a pull request on GitHub! Before a pull request gets merged, it should be **rebased** onto master and squashed into a minimal set of commits. Each commit should include the necessary code, test, and documentation changes for a single "piece" of functionality. To be mergable, patches must: - be rebased onto the latest master, - be automatically mergeable, - not break existing tests (TravisCI_ will run them, too), - not change existing tests without a *very* good reason, - add tests for new code (bug fixes should include regression tests, new features should have relevant tests), - not introduce any new flake8_ errors (run ``./run.sh lint``), - include updated source translations (run ``./run.sh makemessages`` and ``./run.sh compilemessages``), - document any new features, and - have a `good commit message`_. Regressions tests should fail without the rest of the patch and pass with it. .. _open a new issue: https://github.com/django-waffle/django-waffle/issues/new .. _Fork: https://github.com/django-waffle/django-waffle/fork .. _TravisCI: https://travis-ci.org/django-waffle/django-waffle .. _flake8: https://pypi.python.org/pypi/flake8 .. _good commit message: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html django-waffle-0.20.0/LICENSE000066400000000000000000000027611362207625100153140ustar00rootroot00000000000000Copyright (c) 2015 James Socol All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. 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. 3. Neither the name of django-waffle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 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 OWNER 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. django-waffle-0.20.0/MANIFEST.in000066400000000000000000000001621362207625100160360ustar00rootroot00000000000000include LICENSE include README.rst include waffle/templates/waffle/waffle.js recursive-include waffle/locale *.mo django-waffle-0.20.0/README.rst000066400000000000000000000012771362207625100157770ustar00rootroot00000000000000====== README ====== Django Waffle is (yet another) feature flipper for Django. You can define the conditions for which a flag should be active, and use it in a number of ways. .. image:: https://travis-ci.org/django-waffle/django-waffle.svg?branch=master :target: https://travis-ci.org/django-waffle/django-waffle :alt: Travis-CI Build Status .. image:: https://badge.fury.io/py/django-waffle.svg :target: https://badge.fury.io/py/django-waffle :alt: PyPI status badge :Code: https://github.com/django-waffle/django-waffle :License: BSD; see LICENSE file :Issues: https://github.com/django-waffle/django-waffle/issues :Documentation: https://waffle.readthedocs.io/ django-waffle-0.20.0/RELEASING.rst000066400000000000000000000007771362207625100163570ustar00rootroot00000000000000Releasing New Versions ====================== These are the steps necessary to release a new version of Django Waffle. 1. Update the version number in the following files: a. `setup.py` b. `docs/conf.py` c. `waffle/__init__.py` 2. Update the changelog in `CHANGES`. 3. Merge these changes to the `master` branch. 4. Create a new release on GitHub. This will also create a Git tag, and trigger a push to PyPI. 5. Ensure the documentation build passes: https://readthedocs.org/projects/waffle/ django-waffle-0.20.0/docs/000077500000000000000000000000001362207625100152315ustar00rootroot00000000000000django-waffle-0.20.0/docs/Makefile000066400000000000000000000127301362207625100166740ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # 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 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 " 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 " 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 " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 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-waffle.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-waffle.qhc" 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-waffle" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-waffle" @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." 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." django-waffle-0.20.0/docs/about/000077500000000000000000000000001362207625100163435ustar00rootroot00000000000000django-waffle-0.20.0/docs/about/contributing.rst000066400000000000000000000042301362207625100216030ustar00rootroot00000000000000.. _about-contributing: .. highlight:: shell ====================== Contributing to Waffle ====================== Waffle is pretty simple to hack, and has a decent test suite! Here's how to patch Waffle, add tests, run them, and contribute changes. **Please** `open a new issue`_ to discuss a new feature before beginning work on it. Not all suggestions are accepted. The :ref:`Goals ` may help guide which features are likely to be accepted. Set Up ====== Setting up an environment is easy! You'll want ``virtualenv`` and ``pip``, then just create a new virtual environment and install the requirements:: $ mkvirtualenv waffle $ pip install -r requirements.txt Done! Writing Patches =============== Fork_ Waffle and create a new branch off master for your patch. Run the tests often:: $ ./run.sh test Try to keep each branch to a single feature or bugfix. .. note:: To update branches, please **rebase** onto master, do not merge master into your branch. Submitting Patches ================== Open a pull request on GitHub! Before a pull request gets merged, it should be **rebased** onto master and squashed into a minimal set of commits. Each commit should include the necessary code, test, and documentation changes for a single "piece" of functionality. To be mergable, patches must: - be rebased onto the latest master, - be automatically mergeable, - not break existing tests (TravisCI_ will run them, too), - not change existing tests without a *very* good reason, - add tests for new code (bug fixes should include regression tests, new features should have relevant tests), - not introduce any new flake8_ errors (run ``./run.sh lint``), - document any new features, and - have a `good commit message`_. Regressions tests should fail without the rest of the patch and pass with it. .. _open a new issue: https://github.com/django-waffle/django-waffle/issues/new .. _Fork: https://github.com/django-waffle/django-waffle/fork .. _TravisCI: https://travis-ci.org/django-waffle/django-waffle .. _flake8: https://pypi.python.org/pypi/flake8 .. _good commit message: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html django-waffle-0.20.0/docs/about/goals.rst000066400000000000000000000007751362207625100202130ustar00rootroot00000000000000.. _about-goals: ============== Waffle's goals ============== .. note:: This document is a work in progress. See :ref:`the roadmap `, too. Waffle is designed to - support continuous integration and deployment, - support feature rollout, - with minimum set-up time and learning, - while covering common segments, - and being fast and robust enough for production use. Waffle is **not** designed to - be secure, or be a replacement for permissions, - cover all potential segments. django-waffle-0.20.0/docs/about/roadmap.rst000066400000000000000000000113021362207625100205150ustar00rootroot00000000000000.. _about-roadmap: ======= Roadmap ======= .. note:: This roadmap is subject to change, but represents the rough direction I plan to go. For specific issues, see the current milestones_. Waffle is already a useful library used in many production systems, but it is not done evolving. Present through pre-1.0 ======================= The immediate future is finishing common segment features and bug fixes. 0.10.2–0.11.x ------------- 0.10.2_ was primarily a docs overhaul with a major fix to how caching works. It was combined with 0.11_. It did include test utilities for consumers. 0.11_ updated support, dropping 1.5 and adding 1.8, and overhauled Jinja integration to be compatible with any Jinja2 helper, like jingo or—more future-proof—django-jinja_. 0.11.1 is probably the last release of the 0.11.x_ series. It added support for Django 1.9 without deprecating any other versions. 0.12 ---- 0.12_ includes a couple of significant refactors designed to pay down some of the debt that's accrued in the past few years. It also includes support for Django 1.10 and above. 0.13 ---- 0.13_ drops support for all versions of Django prior to 1.8, including dropping South migrations (and finally being rid of the old issues with them). Along with that, it changes the way settings are configured to be more modern. 0.13 is about closing some long-standing feature gaps, like segmenting by IP and User-Agent. It also includes finally making a decision about auto-create/data-in-settings. Toward 1.0 ========== There are no solid criteria for what makes 1.0 right now, but after 0.13, most outstanding issues will be resolved and Waffle will be in very good shape. There are no plans for a 0.14, so it seems likely that the next step after 0.13 would be some clean-up and finally a 1.0. Beyond 1.0 ========== *tl;dr: Waffle2 may be a complete break from Waffle.* Waffle is one of the first Python libraries I created, you can see that in the amount of code I left in ``__init__.py``. It is also 5 years old, and was created during a different period in my career, and in Django. There are some philosophical issues with how Waffle is designed. Adding new methods of segmenting users requires at least one new column each, and increasing the cyclomatic complexity. Caching is difficult. The requirements are stringent and no longer realistic (they were created before Django 1.5). The distinction between Flags, Samples, and Switches is confusing and triples the API surface area (Flags can easily act as Switches, less easily as Samples). It is not extensible. Some challenges also just accrue over time. Dropping support for Django 1.4, the current Extended Support Release, would significantly simplify a few parts. There is a simplicity to Waffle that I've always appreciated vs, say, Gargoyle_. Not least of which is that Waffle works with the built-in admin (or any other admin you care to use). I don't have to write any code to start using Waffle, other than an ``if`` block. Just add a row and click some checkboxes. Most batteries are included. These are all things that any new version of Waffle must maintain. Still, if I *want* to write code to do some kind of custom segment that isn't common-enough to belong in Waffle, shouldn't I be able to? (And, if all the core segmenters were built as the same kind of extension, we could lower the bar for inclusion.) If I only care about IP address and percentage, it would be great to skip all the other checks that just happen to be higher in the code. I have rough sketches of what this looks like, but there are still some significant sticking points, particularly around shoehorning all of this into the existing Django admin. I believe it's *possible*, just potentially *gross*. (Then again, if it's gross underneath but exposes a pleasant UI, that's not ideal, but it's OK.) The other big sticking point is that this won't be a simple ``ALTER TABLE wafle_flag ADD COLUMN`` upgrade; things will break. I've been thinking what Waffle would be like if I designed it from scratch today with slightly different goals, like extensibility. Beyond 1.0, it's difficult to see continuing to add new features without this kind of overhaul. .. _milestones: https://github.com/django-waffle/django-waffle/milestones .. _0.10.2: https://github.com/django-waffle/django-waffle/milestones/0.10.2 .. _0.11: https://github.com/django-waffle/django-waffle/milestones/0.11 .. _0.11.x: https://github.com/django-waffle/django-waffle/milestones/0.11.x .. _0.12: https://github.com/django-waffle/django-waffle/milestones/0.12 .. _0.13: https://github.com/django-waffle/django-waffle/milestones/0.13 .. _Gargoyle: https://github.com/disqus/gargoyle .. _django-jinja: https://niwinz.github.io/django-jinja/latest/django-waffle-0.20.0/docs/about/why-waffle.rst000066400000000000000000000030441362207625100211470ustar00rootroot00000000000000.. _about-why-waffle: =========== Why Waffle? =========== `Feature flags`_ are a critical tool for continuously integrating and deploying applications. Waffle is one of `several options`_ for managing feature flags in Django applications. Waffle :ref:`aims to ` - provide a simple, intuitive API everywhere in your application; - cover common use cases with batteries-included; - be simple to install and manage; - be fast and robust enough to use in production; and - minimize dependencies and complexity. Waffle has an `active community`_ and gets `fairly steady updates`_. vs Gargoyle =========== The other major, active feature flag tool for Django is Disqus's Gargoyle_. Both support similar features, though Gargoyle offers more options for building custom segments in exchange for some more complexity and requirements. Waffle in Production ==================== Despite its pre-1.0 version number, Waffle has been used in production for years at places like Mozilla, Yipit and TodaysMeet. - Mozilla (Support, MDN, Addons, etc) - TodaysMeet - Yipit (If you're using Waffle in production and don't mind being included here, let me know or add yourself in a pull request!) .. _Feature flags: http://code.flickr.net/2009/12/02/flipping-out/ .. _several options: https://www.djangopackages.com/grids/g/feature-flip/ .. _active community: https://github.com/django-waffle/django-waffle/graphs/contributors .. _fairly steady updates: https://github.com/django-waffle/django-waffle/pulse/monthly .. _Gargoyle: https://github.com/disqus/gargoyle django-waffle-0.20.0/docs/conf.py000066400000000000000000000171661362207625100165430ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # django-waffle documentation build configuration file, created by # sphinx-quickstart on Wed Aug 1 17:45:05 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.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.mathjax'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'django-waffle' copyright = '2012-2018, James Socol' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.20' # The full version, including alpha/beta/rc tags. release = '0.20.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of 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 = [] # -- 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 = 'nature' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_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 # Output file base name for HTML help builder. htmlhelp_basename = 'django-waffledoc' # -- 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': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'django-waffle.tex', 'django-waffle Documentation', 'James Socol', '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 = [ ('index', 'django-waffle', 'django-waffle Documentation', ['James Socol'], 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 = [ ('index', 'django-waffle', 'django-waffle Documentation', 'James Socol', 'django-waffle', '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' django-waffle-0.20.0/docs/index.rst000066400000000000000000000012271362207625100170740ustar00rootroot00000000000000.. _index: ============= Django Waffle ============= Waffle is feature flipper for Django. You can define the conditions for which a flag should be active, and use it in a number of ways. :Version: |release| :Code: https://github.com/django-waffle/django-waffle :License: BSD; see LICENSE file :Issues: https://github.com/django-waffle/django-waffle/issues Contents: .. toctree:: :titlesonly: about/why-waffle starting/index types/index usage/index testing/index about/contributing about/goals about/roadmap Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` django-waffle-0.20.0/docs/starting/000077500000000000000000000000001362207625100170645ustar00rootroot00000000000000django-waffle-0.20.0/docs/starting/configuring.rst000066400000000000000000000106121362207625100221300ustar00rootroot00000000000000.. _starting-configuring: ================== Configuring Waffle ================== There are a few global settings you can define to adjust Waffle's behavior. ``WAFFLE_COOKIE`` The format for the cookies Waffle sets. Must contain ``%s``. Defaults to ``dwf_%s``. ``WAFFLE_FLAG_DEFAULT`` When a Flag is undefined in the database, Waffle considers it ``False``. Set this to ``True`` to make Waffle consider undefined flags ``True``. Defaults to ``False``. ``WAFFLE_FLAG_MODEL`` The model that will be use to keep track of flags. Defaults to ``waffle.Flag`` which allows user- and group-based flags. Can be swapped for a different Flag model that allows flagging based on other things, such as an organization or a company that a user belongs to. Analogous functionality to Django's extendable User models. Needs to be set at the start of a project, as the Django migrations framework does not support changing swappable models after the initial migration. ``WAFFLE_SWITCH_DEFAULT`` When a Switch is undefined in the database, Waffle considers it ``False``. Set this to ``True`` to make Waffle consider undefined switches ``True``. Defaults to ``False``. ``WAFFLE_SAMPLE_DEFAULT`` When a Sample is undefined in the database, Waffle considers it ``False``. Set this to ``True`` to make Waffle consider undefined samples ``True``. Defaults to ``False``. ``WAFFLE_MAX_AGE`` How long should Waffle cookies last? (Integer, in seconds.) Defaults to ``2529000`` (one month). ``WAFFLE_READ_FROM_WRITE_DB`` When calling ``*_is_active`` methods, Waffle attempts to retrieve a cached version of the object, falling back to the database if necessary. In high- traffic scenarios with multiple databases (e.g. a primary being replicated to a readonly pool) this introduces the risk that a stale version of the object might be cached if one of these methods is called immediately after an update. Set this to ``True`` to ensure Waffle always reads Flags, Switches, and Samples from the DB configured for writes on cache misses. ``WAFFLE_OVERRIDE`` Allow *all* Flags to be controlled via the querystring (to allow e.g. Selenium to control their behavior). Defaults to ``False``. ``WAFFLE_SECURE`` Whether to set the ``secure`` flag on cookies. Defaults to ``True``. ``WAFFLE_CACHE_PREFIX`` Waffle tries to store objects in cache pretty aggressively. If you ever upgrade and change the shape of the objects (for example upgrading from <0.7.5 to >0.7.5) you'll want to set this to something other than ``'waffle:'``. If you're using memcached this should be ASCII only, as that's all it supports. ``WAFFLE_CACHE_NAME`` Which cache to use. Defaults to ``'default'``. ``WAFFLE_CREATE_MISSING_FLAGS`` If Waffle encounters a reference to a flag that is not in the database, should Waffle create the flag? If true new flags are created and set to the value of ``WAFFLE_FLAG_DEFAULT`` Defaults to ``False``. ``WAFFLE_CREATE_MISSING_SWITCHES`` If Waffle encounters a reference to a switch that is not in the database, should Waffle create the sample? If true new switches are created and set to the value of ``WAFFLE_SWITCH_DEFAULT`` Defaults to ``False``. ``WAFFLE_CREATE_MISSING_SAMPLES`` If Waffle encounters a reference to a sample that is not in the database, should Waffle create the sample? If true new samples are created and set to the value of ``WAFFLE_SAMPLE_DEFAULT`` Defaults to ``False``. ``WAFFLE_LOG_MISSING_FLAGS`` If Waffle encounters a reference to a flag that is not in the database, should Waffle log it? The value describes the level of wanted warning, possible values are all levels know by pythons default logging, e.g. ``logging.WARNING``. Defaults to ``None``. ``WAFFLE_LOG_MISSING_SWITCHES`` If Waffle encounters a reference to a switch that is not in the database, should Waffle log it? The value describes the level of wanted warning, possible values are all levels know by pythons default logging, e.g. ``logging.WARNING``. Defaults to ``None``. ``WAFFLE_LOG_MISSING_SAMPLES`` If Waffle encounters a reference to a sample that is not in the database,, should Waffle log it? The value describes the level of wanted warning, possible values are all levels know by pythons default logging, e.g. ``logging.WARNING``. Defaults to ``None``. django-waffle-0.20.0/docs/starting/index.rst000066400000000000000000000002401362207625100207210ustar00rootroot00000000000000.. _starting-index: =============== Getting Started =============== .. toctree:: :titlesonly: requirements installation upgrading configuring django-waffle-0.20.0/docs/starting/installation.rst000066400000000000000000000051651362207625100223260ustar00rootroot00000000000000.. _starting-installation: ============ Installation ============ After ensuring that the :ref:`requirements ` are met, installing Waffle is a simple process. Getting Waffle ============== Waffle is `hosted on PyPI`_ and can be installed with ``pip`` or ``easy_install``: .. code-block:: shell $ pip install django-waffle $ easy_install django-waffle Waffle is also available `on GitHub`_. In general, ``master`` should be stable, but use caution depending on unreleased versions. .. _hosted on PyPI: http://pypi.python.org/pypi/django-waffle .. _on GitHub: https://github.com/django-waffle/django-waffle .. _installation-settings: Settings ======== Add ``waffle`` to the ``INSTALLED_APPS`` setting, and ``waffle.middleware.WaffleMiddleware`` to ``MIDDLEWARE``, e.g.:: INSTALLED_APPS = ( # ... 'waffle', # ... ) MIDDLEWARE = ( # ... 'waffle.middleware.WaffleMiddleware', # ... ) .. _installation-settings-templates: Jinja Templates --------------- .. versionchanged:: 0.19 If you are using Jinja2 templates, the ``django-jinja`` dependency is currently unavailable with django 3.0 and greater; 2.x versions are compatible as well as 1.11. .. versionchanged:: 0.11 If you're using Jinja2 templates, Waffle provides a Jinja2 extension (``waffle.jinja.WaffleExtension``) to :ref:`use Waffle directly from templates `. How you install this depends on which adapter you're using. With django-jinja_, add the extension to the ``extensions`` list:: TEMPLATES = [ { 'BACKEND': 'django_jinja.backend.Jinja2', 'OPTIONS': { 'extensions': [ # ... 'waffle.jinja.WaffleExtension', ], # ... }, # ... }, # ... ] With jingo_, add it to the ``JINJA_CONFIG['extensions']`` list:: JINJA_CONFIG = { 'extensions': [ # ... 'waffle.jinja.WaffleExtension', ], # ... } Database Schema =============== Waffle includes `Django migrations`_ for creating the correct database schema. If using Django >= 1.7, simply run the ``migrate`` management command after adding Waffle to ``INSTALLED_APPS``: .. code-block:: shell $ django-admin.py migrate If you're using a version of Django without migrations, you can run ``syncdb`` to create the Waffle tables. .. _Django migrations: https://docs.djangoproject.com/en/dev/topics/migrations/ .. _django-jinja: https://pypi.python.org/pypi/django-jinja/ .. _jingo: http://jingo.readthedocs.org/ django-waffle-0.20.0/docs/starting/requirements.rst000066400000000000000000000027551362207625100223520ustar00rootroot00000000000000.. _starting-requirements: ============ Requirements ============ Waffle depends only on Django (except for :ref:`running Waffle's tests `) but does require certain Django features. User Models =========== Waffle requires Django's `auth system`_, in particular it requires both a user model and Django's groups. If you're using a `custom user model`_, this can be accomplished by including Django's `PermissionsMixin`_, e.g.:: from django.contrib.auth import models class MyUser(models.AbstractBaseUser, models.PermissionsMixin): And of ``django.contrib.auth`` must be in ``INSTALLED_APPS``, along with `its requirements`_. .. _auth system: https://docs.djangoproject.com/en/dev/topics/auth/ .. _custom user model: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#specifying-a-custom-user-model .. _PermissionsMixin: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#custom-users-and-permissions .. _its requirements: https://docs.djangoproject.com/en/dev/topics/auth/#installation Templates ========= Waffle provides template tags to check flags directly in templates. Using these requires the ``request`` object in the template context, which can be easily added with the ``request`` `template context processor`_:: TEMPLATE_CONTEXT_PROCESSORS = ( # ... 'django.template.context_processors.request', # ... .. _template context processor: https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors django-waffle-0.20.0/docs/starting/upgrading.rst000066400000000000000000000007231362207625100216000ustar00rootroot00000000000000.. _starting-upgrading: ========= Upgrading ========= From v0.10.x to v0.11 ===================== Jinja2 Templates ---------------- Waffle no longer supports `jingo's ` automatic helper import, but now ships with a `Jinja2 ` extension that supports multiple Jinja2 template loaders for Django. See the :ref:`installation docs ` for details on how to install this extension. django-waffle-0.20.0/docs/testing/000077500000000000000000000000001362207625100167065ustar00rootroot00000000000000django-waffle-0.20.0/docs/testing/automated.rst000066400000000000000000000043451362207625100214310ustar00rootroot00000000000000.. _testing-automated: ============================= Automated testing with Waffle ============================= Feature flags present a new challenge for writing tests. The test database may not have Flags, Switches, or Samples defined, or they may be non-deterministic. My philosophy, and one I encourage you to adopt, is that tests should cover *both* code paths, with any feature flags on and off. To do this, you'll need to make the code behave deterministically. Here, I'll cover some tips and best practices for testing your app while using feature flags. I'll talk specifically about Flags but this can equally apply to Switches or Samples. Unit tests ========== Waffle provides three context managers (that can also be used as decorators) in ``waffle.testutils`` that make testing easier. - ``override_flag`` - ``override_sample`` - ``override_switch`` All three are used the same way:: with override_flag('flag_name', active=True): # Only 'flag_name' is affected, other flags behave normally. assert waffle.flag_is_active(request, 'flag_name') Or:: @override_sample('sample_name', active=True) def test_with_sample(): # Only 'sample_name' is affected, and will always be True. Other # samples behave normally. assert waffle.sample_is_active('sample_name') All three will restore the relevant flag, sample, or switch to its previous state: they will restore the old values and will delete objects that did not exist. External test suites ==================== Tests that run in a separate process, such as Selenium tests, may not have access to the test database or the ability to mock Waffle values. For tests that make HTTP requests to the system-under-test (e.g. with Selenium_ or PhantomJS_) the ``WAFFLE_OVERRIDE`` :ref:`setting ` makes it possible to control the value of any *Flag* via the querystring. .. highlight:: http For example, for a flag named ``foo``, we can ensure that it is "on" for a request:: GET /testpage?foo=1 HTTP/1.1 or that it is "off":: GET /testpage?foo=0 HTTP/1.1 .. _mock: http://pypi.python.org/pypi/mock/ .. _fudge: http://farmdev.com/projects/fudge/ .. _Selenium: http://www.seleniumhq.org/ .. _PhantomJS: http://phantomjs.org/ django-waffle-0.20.0/docs/testing/index.rst000066400000000000000000000024371362207625100205550ustar00rootroot00000000000000.. _testing-index: =================== Testing with Waffle =================== "Testing" takes on at least two distinct meanings with Waffle: - Testing your application with automated tools - Testing your feature with users For the purposes of this chapter, we'll refer to the former as "automated testing" and the latter as "user testing" for clarity. .. toctree:: :maxdepth: 1 automated user Automated testing ================= Automated testing encompasses things like unit and integration tests, whether they use the Python/Django unittest framework or an external tool like Selenium. Waffle is often non-deterministic, i.e. it introduces true randomness to the system-under-test, which is a nightmare for automated testing. Thus, Waffle includes tools to re-introduce determinism in automated test suites. :ref:`Read more about automated testing `. User testing ============ User testing occurs on both a (relatively) large scale with automated metric collection and on a small, often one-to-one—such as testing sessions with a user and research or turning on a feature within a company or team. Waffle does what it can to support these kinds of tests while still remaining agnostic about metrics platforms. :ref:`Read more about user testing `. django-waffle-0.20.0/docs/testing/user.rst000066400000000000000000000113571362207625100204250ustar00rootroot00000000000000.. _testing-user: ======================== User testing with Waffle ======================== Testing a feature (i.e. not :ref:`testing the code `) with users usually takes one of two forms: small-scale tests with individuals or known group, and large-scale tests with a subset of production users. Waffle provides tools for the former and has some suggestions for the latter. Small-scale tests ================= There are two ways to control a flag for an individual user: - add their account to the flag's list of users, or - use testing mode. Testing mode makes it possible to enable a flag via a querystring parameter (like ``WAFFLE_OVERRIDE``) but is unique for two reasons: - it can be enabled and disabled on a flag-by-flag basis, and - it only requires the querystring parameter once, then relies on cookies. If the flag we're testing is called ``foo``, then we can enable testing mode, and send users to ``oursite.com/testpage?dwft_foo=1`` (or ``=0``) and the flag will be on (or off) for them for the remainder of their session. .. warning:: Currently, the flag **must** be used by the first page they visit, or the cookie will not get set. See `#80`_ on GitHub. Researchers can send a link with these parameters to anyone and then observe or ask questions. At the end of their session, or when testing mode is deactivated, they will call back to normal behavior. For a small group, like a company or team, it may be worth creating a Django group and adding or removing the group from the flag. Large-scale tests ================= Large scale tests are tests along the lines of "roll this out to 5% of users and observe the relevant metrics." Since "the relevant metrics" is very difficult to define across all sites, here are some thoughts from my experience with these sorts of tests. Client-side metrics ------------------- Google Analytics—and I imagine similar products—has the ability to segment by page or `session variables`_. If you want to A/B test a conversion rate or funnel, or otherwise measure the impact on some client-side metric, using these variables is a solid way to go. For example, in GA, you might do the following to A/B test a landing page: .. code-block:: django ga('set', 'dimension1', 'Landing Page Version {% flag "new_landing_page" %}2{% else %}1{% endif %}'); Similarly you might set session or visitor variables for funnel tests. The exact steps to both set a variable like this and then to create segments and examine the data will depend on your client-side analytics tool. And, of course, this can be combined with other data and further segmented if you need to. Server-side metrics ------------------- I use StatsD_ religiously. Sometimes Waffle is useful for load and capacity testing in which case I want to observe timing data or error rates. Sometimes, it makes sense to create entirely new metrics, and measure them directly, e.g.:: if flag_is_active('image-process-service'): with statsd.timer('imageservice'): try: processed = make_call_to_service(data) except ServiceError: statsd.incr('imageservice.error') else: statsd.incr('imageservice.success') else: with statsd.timer('process-image'): processed = do_inline_processing(data) Other times, existing data—e.g. timers on the whole view—isn't going to move. If you have enough data to be statistically meaningful, you can measure the impact for a given proportion of traffic and derive the time for the new code. If a flag enabling a refactored codepath is set to 20% of users, and average time has improved by 10%, you can calculate that you've improved the speed by 50%! You can use the following to figure out the average for requests using the new code. Let :math:`t_{old}` be the average time with the flag at 0%, :math:`t_{total}` be the average time with the flag at :math:`p * 100%`. Then the average for requests using new code, :math:`t_{new}` is... .. math:: t_{new} = t_{old} - \frac{t_{old} - t_{total}}{p} If you believe my math (you should check it!) then you can measure the average with the flag at 0% to get :math:`t_{old}` (let's say 1.2 seconds), then at :math:`p * 100` % (let's say 20%, so :math:`p = 0.2`) to get :math:`t_{total}` (let's say 1.08 seconds, a 10% improvement) and you have enough to get the average of the new path. .. math:: t_{new} = 1.2 - \frac{1.2 - 1.08}{0.2} = 0.6 Wow, good work! You can use similar methods to derive the impact on other factors. .. _session variables: https://developers.google.com/analytics/devguides/collection/upgrade/reference/gajs-analyticsjs#custom-vars .. _#80: https://github.com/django-waffle/django-waffle/issues/80 .. _StatsD: https://github.com/etsy/statsd django-waffle-0.20.0/docs/types/000077500000000000000000000000001362207625100163755ustar00rootroot00000000000000django-waffle-0.20.0/docs/types/flag.rst000066400000000000000000000204531362207625100200440ustar00rootroot00000000000000.. _types-flag: ===== Flags ===== Flags are the most robust, flexible method of rolling out a feature with Waffle. Flags can be used to enable a feature for specific users, groups, users meeting certain criteria (such as being authenticated, or superusers) or a certain percentage of visitors. How Flags Work ============== Flags compare the current request_ to their criteria to decide whether they are active. Consider this simple example:: if flag_is_active(request, 'foo'): pass The :ref:`flag_is_active ` function takes two arguments, the request, and the name of a flag. Assuming this flag (``foo``) is defined in the database, Waffle will make roughly the following decisions: - Is ``WAFFLE_OVERRIDE`` active and if so does this request specify a value for this flag? If so, use that value. - If not, is the flag set to globally on or off (the *Everyone* setting)? If so, use that value. - If not, is the flag in *Testing* mode, and does the request specify a value for this flag? If so, use that value and set a testing cookie. - If not, does the current user meet any of our criteria? If so, the flag is active. - If not, does the user have an existing cookie set for this flag? If so, use that value. - If not, randomly assign a value for this user based on the *Percentage* and set a cookie. Flag Attributes =============== Flags can be administered through the Django `admin site`_ or the :ref:`command line `. They have the following attributes: :Name: The name of the flag. Will be used to identify the flag everywhere. :Everyone: Globally set the Flag, **overriding all other criteria**. Leave as *Unknown* to use other criteria. :Testing: Can the flag be specified via a querystring parameter? :ref:`See below `. :Percent: A percentage of users for whom the flag will be active, if no other criteria applies to them. :Superusers: Is this flag always active for superusers? :Staff: Is this flag always active for staff? :Authenticated: Is this flag always active for authenticated users? :Languages: Is the ``LANGUAGE_CODE`` of the request in this list? (Comma-separated values.) :Groups: A list of group IDs for which this flag will always be active. :Users: A list of user IDs for which this flag will always be active. :Rollout: Activate Rollout mode? :ref:`See below `. :Note: Describe where the flag is used. A Flag will be active if *any* of the criteria are true for the current user or request (i.e. they are combined with ``or``). For example, if a Flag is active for superusers, a specific group, and 12% of visitors, then it will be active if the current user is a superuser *or* if they are in the group *or* if they are in the 12%. .. note:: Users are assigned randomly when using Percentages, so in practice the actual proportion of users for whom the Flag is active will probably differ slightly from the Percentage value. .. _types-flag-custom-model: Custom Flag Models ====================== For many cases, the default Flag model provides all the necessary functionality. It allows flagging individual Users and Groups. If you would like flags to be applied to different things, such as companies a User belongs to, you can use a custom flag model. The functionality uses the same concepts as Django's custom user models, and a lot of this will be immediately recognizable. An application needs to define a ``WAFFLE_FLAG_MODEL`` settings. The default is ``waffle.Flag`` but can be pointed to an arbitrary object. .. note:: It is not possible to change the Flag model and generate working migrations. Ideally, the flag model should be defined at the start of a new project. This is a limitation of the `swappable` Django magic. Please use magic responsibly. The custom Flag model must inherit from `waffle.models.AbstractBaseFlag`. If you want the existing ``User`` and ``Group`` based flagging and would like to add more entities to it, you may extend `waffle.models.AbstractUserFlag`. If you need to reference the class that is being used as the `Flag` model in your project, use the ``get_waffle_flag_model()`` method. If you reference the Flag a lot, it may be convenient to add ``Flag = get_waffle_flag_model()`` right below your imports and reference the Flag model as if it had been imported directly. Example: .. code-block:: python # settings.py WAFFLE_FLAG_MODEL = 'myapp.Flag' # models.py class Flag(AbstractUserFlag): FLAG_COMPANIES_CACHE_KEY = 'FLAG_COMPANIES_CACHE_KEY' FLAG_COMPANIES_CACHE_KEY_DEFAULT = 'flag:%s:companies' companies = models.ManyToManyField( Company, blank=True, help_text=_('Activate this flag for these companies.'), ) def get_flush_keys(self, flush_keys=None): flush_keys = super(Flag, self).get_flush_keys(flush_keys) companies_cache_key = get_setting(Flag.FLAG_COMPANIES_CACHE_KEY, Flag.FLAG_COMPANIES_CACHE_KEY_DEFAULT) flush_keys.append(keyfmt(companies_cache_key, self.name)) return flush_keys def is_active_for_user(self, user): is_active = super(Flag, self).is_active_for_user(user) if is_active: return is_active if getattr(user, 'company_id', None): company_ids = self._get_company_ids() if user.company_id in company_ids: return True def _get_company_ids(self): cache_key = keyfmt( get_setting(Flag.FLAG_COMPANIES_CACHE_KEY, Flag.FLAG_COMPANIES_CACHE_KEY_DEFAULT), self.name ) cached = cache.get(cache_key) if cached == CACHE_EMPTY: return set() if cached: return cached company_ids = set(self.companies.all().values_list('pk', flat=True)) if not company_ids: cache.add(cache_key, CACHE_EMPTY) return set() cache.add(cache_key, company_ids) return company_ids # admin.py from waffle.admin import FlagAdmin as WaffleFlagAdmin class FlagAdmin(WaffleFlagAdmin): raw_id_fields = tuple(list(WaffleFlagAdmin.raw_id_fields) ['companies']) admin.site.register(Flag, FlagAdmin) .. _types-flag-testing: Testing Mode ============ See :ref:`User testing with Waffle `. .. _types-flag-rollout: Rollout Mode ============ When a Flag is activated by chance, Waffle sets a cookie so the flag will not flip back and forth on subsequent visits. This can present a problem for gradually deploying new features: users can get "stuck" with the Flag turned off, even as the percentage increases. *Rollout mode* addresses this by changing the TTL of "off" cookies. When Rollout mode is active, cookies setting the Flag to "off" are session cookies, while those setting the Flag to "on" are still controlled by :ref:`WAFFLE_MAX_AGE `. Effectively, Rollout mode changes the *Percentage* from "percentage of visitors" to "percent chance that the Flag will be activated per visit." .. _request: https://docs.djangoproject.com/en/dev/topics/http/urls/#how-django-processes-a-request .. _admin site: https://docs.djangoproject.com/en/dev/ref/contrib/admin/ .. _types-flag-auto-create-missing: Auto Create Missing =================== When a flag is evaluated in code that is missing in the database the flag returns the :ref:`WAFFLE_FLAG_DEFAULT ` value but does not create a flag in the database. If you'd like waffle to create missing flags in the database whenever it encounters a missing flag you can set :ref:`WAFFLE_CREATE_MISSING_FLAGS ` to ``True``. Missing flags will be created in the database and the value of the ``Everyone`` flag attribute will be set to :ref:`WAFFLE_FLAG_DEFAULT ` in the auto-created database record. .. _types-flag-log-missing: Log Missing =================== Whether or not you enabled :ref:`Auto Create Missing Flags `, it can be practical to be informed that a flag was or is missing. If you'd like waffle to log a warning, error, ... you can set :ref:`WAFFLE_LOG_MISSING_FLAGS ` to any level known by Python default logger. django-waffle-0.20.0/docs/types/index.rst000066400000000000000000000002221362207625100202320ustar00rootroot00000000000000.. _types-index: ===== Types ===== Waffle supports three types of feature flippers: .. toctree:: :maxdepth: 1 flag switch sample django-waffle-0.20.0/docs/types/sample.rst000066400000000000000000000044731362207625100204200ustar00rootroot00000000000000.. _types-sample: ======= Samples ======= Samples are on a given percentage of the time. They do not require a request object and can be used in other contexts, such as management commands and tasks. .. warning:: Sample values are random: if you check a Sample twice, there is no guarantee you will get the same value both times. If you need to rely on the value more than once, you should store it in a variable. :: # YES foo_on = sample_is_active('foo') if foo_on: pass # ...later... if foo_on: pass :: # NO! if sample_is_active('foo'): pass # ...later... if sample_is_active('foo'): # INDEPENDENT of the previous check pass Sample Attributes ================= Samples can be administered through the Django `admin site`_ or the :ref:`command line `. They have the following attributes: :Name: The name of the Sample. :Percent: A number from 0.0 to 100.0 that determines how often the Sample will be active. :Note: Describe where the Sample is used. .. _admin site: https://docs.djangoproject.com/en/dev/ref/contrib/admin/ .. _types-sample-auto-create-missing: Auto Create Missing =================== When a sample is evaluated in code that is missing in the database the sample returns the :ref:`WAFFLE_SAMPLE_DEFAULT ` value but does not create a sample in the database. If you'd like waffle to create missing samples in the database whenever it encounters a missing sample you can set :ref:`WAFFLE_CREATE_MISSING_SAMPLES ` to ``True``. If :ref:`WAFFLE_SAMPLE_DEFAULT ` is ``True`` then the ``Percent`` attribute of the sample will be created as 100.0 (so that when the sample is checked it always evaluates to ``True``). Otherwise the value will be set to 0.0 so that the sample always evaluates to ``False``. .. _types-sample-log-missing: Log Missing =================== Whether or not you enabled :ref:`Auto Create Missing Sample `, it can be practical to be informed that a sample was or is missing. If you'd like waffle to log a warning, error, ... you can set :ref:`WAFFLE_LOG_MISSING_SAMPLES ` to any level known by Python default logger. django-waffle-0.20.0/docs/types/switch.rst000066400000000000000000000032151362207625100204310ustar00rootroot00000000000000.. _types-switch: ======== Switches ======== Switches are simple booleans: they are on or off, for everyone, all the time. They do not require a request object and can be used in other contexts, such as management commands and tasks. Switch Attributes ================= Switches can be administered through the Django `admin site`_ or the :ref:`command line `. They have the following attributes: :Name: The name of the Switch. :Active: Is the Switch active or inactive. :Note: Describe where the Switch is used. .. _admin site: https://docs.djangoproject.com/en/dev/ref/contrib/admin/ .. _types-switch-auto-create-missing: Auto Create Missing =================== When a switch is evaluated in code that is missing in the database the switch returns the :ref:`WAFFLE_SWITCH_DEFAULT ` value but does not create a switch in the database. If you'd like waffle to create missing switches in the database whenever it encounters a missing switch you can set :ref:`WAFFLE_CREATE_MISSING_SWITCHES ` to ``True``. Missing switches will be created in the database and the value of the ``Active`` switch attribute will be set to :ref:`WAFFLE_SWITCH_DEFAULT ` in the auto-created database record. .. _types-switch-log-missing: Log Missing =================== Whether or not you enabled :ref:`Auto Create Missing Switch `, it can be practical to be informed that a switch was or is missing. If you'd like waffle to log a warning, error, ... you can set :ref:`WAFFLE_LOG_MISSING_FLAGS ` to any level known by Python default logger. django-waffle-0.20.0/docs/usage/000077500000000000000000000000001362207625100163355ustar00rootroot00000000000000django-waffle-0.20.0/docs/usage/cli.rst000066400000000000000000000043441362207625100176430ustar00rootroot00000000000000.. _usage-cli: .. highlight:: shell ========================================== Managing Waffle data from the command line ========================================== Aside the Django admin interface, you can use the command line tools to manage all your waffle objects. Flags ===== Use ``manage.py`` to change the values of your flags:: $ ./manage.py waffle_flag name-of-my-flag --everyone --percent=47 Use ``--everyone`` to turn on and ``--deactivate`` to turn off the flag. Set a percentage with ``--percent`` or ``-p``. Set the flag on for superusers (``--superusers``), staff (``--staff``) or authenticated (``--authenticated``) users. Set the rollout mode on with ``--rollout`` or ``-r``. If the flag doesn't exist, add ``--create`` to create it before setting its values:: $ ./manage.py waffle_flag name-of-my-flag --deactivate --create To list all the existing flags, use ``-l``:: $ ./manage.py waffle_flag -l Flags: name-of-my-flag Switches ======== Use ``manage.py`` to change the values of your switches:: $ ./manage.py waffle_switch name-of-my-switch off You can set a switch to ``on`` or ``off``. If that switch doesn't exist, add ``--create`` to create it before setting its value:: $ ./manage.py waffle_switch name-of-my-switch on --create To list all the existing switches, use ``-l``:: $ ./manage.py waffle_switch -l Switches: name-of-my-switch on Samples ======= Use ``manage.py`` to change the values of your samples:: $ ./manage.py waffle_sample name-of-my-sample 100 You can set a sample to any floating value between ``0.0`` and ``100.0``. If that sample doesn't exist, add ``--create`` to create it before setting its value:: $ ./manage.py waffle_sample name-of-my-sample 50.0 --create To list all the existing samples, use ``-l``:: $ ./manage.py waffle_sample -l Samples: name-of-my-sample: 50% Deleting Data ============= Use ``manage.py`` to delete a batch of flags, switches, and/or samples:: $ ./manage.py waffle_delete --switches switch_name_0 switch_name_1 --flags flag_name_0 flag_name_1 --samples sample_name_0 sample_name_1 Pass a list of switch, flag, or sample names to the command as keyword arguments and they will be deleted from the database. django-waffle-0.20.0/docs/usage/decorators.rst000066400000000000000000000026651362207625100212450ustar00rootroot00000000000000.. _usage-decorators: ======================= Decorating entire views ======================= Waffle provides decorators to wrap an entire view in a :ref:`flag ` or :ref:`switch `. (Due to their always-random nature, no decorator is provided for :ref:`samples `.) When the flag or switch is active, the view executes normally. When it is inactive, the view returns a 404. Optionally, you can provide a view or URL name where the decorator can redirect to if you don't want to show a 404 page when the flag or switch is inactive. Flags ===== :: from waffle.decorators import waffle_flag @waffle_flag('flag_name') def myview(request): pass @waffle_flag('flag_name', 'url_name_to_redirect_to') def myotherview(request): pass Switches ======== :: from waffle.decorators import waffle_switch @waffle_switch('switch_name') def myview(request): pass @waffle_switch('switch_name', 'url_name_to_redirect_to') def myotherview(request): pass Inverting Decorators ==================== Both ``waffle_flag`` and ``waffle_switch`` can be reversed (i.e. they will raise a 404 if the flag or switch is *active*, and otherwise execute the view normally) by prepending the name of the flag or switch with an exclamation point: ``!``. :: @waffle_switch('!switch_name') def myview(request): """Only runs if 'switch_name' is OFF.""" django-waffle-0.20.0/docs/usage/index.rst000066400000000000000000000005521362207625100202000ustar00rootroot00000000000000.. _usage-index: ============ Using Waffle ============ Waffle provides a simple API to check the state of :ref:`flags `, :ref:`switches `, and :ref:`samples ` in views and templates, and even on the client in JavaScript. .. toctree:: :titlesonly: views decorators mixins templates javascript cli django-waffle-0.20.0/docs/usage/javascript.rst000066400000000000000000000032111362207625100212320ustar00rootroot00000000000000.. _usage-javascript: ============== Using WaffleJS ============== Waffle supports using :ref:`flags `, :ref:`switches `, and :ref:`samples ` in JavaScript ("WaffleJS") either via inline script or an external script. .. warning:: Unlike samples when used in Python, samples in WaffleJS **are only calculated once** and so are **consistent**. The WaffleJS ``waffle`` object ============================== WaffleJS exposes a global ``waffle`` object that gives access to flags, switches, and samples. Methods ------- These methods can be used exactly like their Python equivalents: - ``waffle.flag_is_active(flag_name)`` - ``waffle.switch_is_active(switch_name)`` - ``waffle.sample_is_active(sample_name)`` Members ------- WaffleJS also directly exposes dictionaries of each type, where keys are the names and values are ``true`` or ``false``: - ``waffle.FLAGS`` - ``waffle.SWITCHES`` - ``waffle.SAMPLES`` Installing WaffleJS =================== As an external script --------------------- Using the ``wafflejs`` view requires adding Waffle to your URL configuration. For example, in your ``ROOT_URLCONF``:: urlpatterns = patterns('', (r'^', include('waffle.urls')), ) This adds a route called ``wafflejs``, which you can use with the ``url`` template tag: .. code-blocK:: django As an inline script ------------------- To avoid an extra request, you can also use the ``wafflejs`` template tag to include WaffleJS as an inline script: .. code-block:: django {% load waffle_tags %} django-waffle-0.20.0/docs/usage/mixins.rst000066400000000000000000000015261362207625100204020ustar00rootroot00000000000000.. _usage-mixins: ============================ Mixins for Class Based Views ============================ Waffle provides mixins to add to Class Based Views. When the flag or switch is active, or a sample returns True, the view executes normally. When it is inactive, the view returns a 404. WaffleFlagMixin =============== .. code-block:: python from waffle.mixins import WaffleFlagMixin class MyClass(WaffleFlagMixin, View): waffle_flag = "my_flag" WaffleSwitchMixin ================= .. code-block:: python from waffle.mixins import WaffleSwitchMixin class MyClass(WaffleSwitchMixin, View): waffle_switch= "my_switch" WaffleSampleMixin ================= .. code-block:: python from waffle.mixins import WaffleSampleMixin class MyClass(WaffleSampleMixin, View): waffle_switch= "my_sample"django-waffle-0.20.0/docs/usage/templates.rst000066400000000000000000000036371362207625100210760ustar00rootroot00000000000000.. _usage-templates: .. highlight:: django ========================= Using Waffle in templates ========================= Waffle makes it easy to test :ref:`flags `, :ref:`switches `, and :ref:`samples ` in templates to flip features on the front-end. It includes support for both Django's built-in templates and for Jinja2_. .. warning:: Before using samples in templates, see the warning in the :ref:`Sample chapter `. .. _templates-django: Django Templates ================ Load the ``waffle_tags`` template tags:: {% load waffle_tags %} In Django templates, Waffle provides three new block types, ``flag``, ``switch``, and ``sample``, that function like ``if`` blocks. Each block supports an optional ``else`` to be rendered if the flag, switch, or sample in inactive. Flags ----- :: {% flag "flag_name" %} flag_name is active! {% else %} flag_name is inactive {% endflag %} Switches -------- :: {% switch "switch_name" %} switch_name is active! {% else %} switch_name is inactive {% endswitch %} Samples ------- :: {% sample "sample_name" %} sample_name is active! {% else %} sample_name is inactive {% endsample %} .. _templates-jinja: Jinja Templates =============== When used with Jinja2_, Waffle provides a ``waffle`` object in the Jinja template context that can be used with normal ``if`` statements. Because these are normal ``if`` statements, you can use ``else`` or ``if not`` as normal. Flags ----- :: {% if waffle.flag('flag_name') %} flag_name is active! {% endif %} Switches -------- :: {% if waffle.switch('switch_name') %} switch_name is active! {% endif %} Samples ------- :: {% if waffle.sample('sample_name') %} sample_name is active! {% endif %} .. _Jinja2: http://jinja.pocoo.org/ django-waffle-0.20.0/docs/usage/views.rst000066400000000000000000000017431362207625100202310ustar00rootroot00000000000000.. _usage-views: ===================== Using Waffle in views ===================== Waffle provides simple methods to test :ref:`flags `, :ref:`switches `, or :ref:`samples ` in views (or, for switches and samples, anywhere else you're writing Python). Flags ===== :: waffle.flag_is_active(request, 'flag_name') Returns ``True`` if the flag is active for this request, else ``False``. For example:: import waffle def my_view(request): if waffle.flag_is_active(request, 'flag_name'): """Behavior if flag is active.""" else: """Behavior if flag is inactive.""" Switches ======== :: waffle.switch_is_active('switch_name') Returns ``True`` if the switch is active, else ``False``. Samples ======= :: waffle.sample_is_active('sample_name') Returns ``True`` if the sample is active, else ``False``. .. warning:: See the warning in the :ref:`Sample chapter `. django-waffle-0.20.0/requirements.txt000066400000000000000000000002161362207625100175640ustar00rootroot00000000000000# These are required to run the tests. Django flake8 six # Everything else is in a Django-version-free version # for TravisCI. -r travis.txt django-waffle-0.20.0/run.sh000077500000000000000000000020451362207625100154450ustar00rootroot00000000000000#!/bin/bash export PYTHONPATH=".:$PYTHONPATH" export DJANGO_SETTINGS_MODULE="test_settings" usage() { echo "USAGE: $0 [command]" echo " test - run the waffle tests" echo " lint - run flake8" echo " shell - open the Django shell" echo " makemigrations - create a schema migration" exit 1 } CMD="$1" shift case "$CMD" in "test" ) DJANGO_SETTINGS_MODULE=test_settings django-admin.py test waffle $@ ;; "lint" ) flake8 waffle $@ ;; "shell" ) django-admin.py shell $@ ;; "makemigrations" ) django-admin.py makemigrations waffle $@ ;; "makemessages" ) export DJANGO_SETTINGS_MODULE= && cd waffle && django-admin.py makemessages && cd - ;; "compilemessages" ) export DJANGO_SETTINGS_MODULE= && cd waffle && django-admin.py compilemessages && cd - ;; "find_uncommitted_translations" ) git diff --exit-code -G "^(msgid|msgstr)" || (echo "Please run ./run.sh makemessages and commit the updated django.po file." && false) ;; * ) usage ;; esac django-waffle-0.20.0/setup.cfg000066400000000000000000000001501362207625100161160ustar00rootroot00000000000000[bdist_wheel] universal = 1 [flake8] exclude=waffle/migrations/*,waffle/south_migrations/* ignore=E731 django-waffle-0.20.0/setup.py000066400000000000000000000023771362207625100160240ustar00rootroot00000000000000from setuptools import setup, find_packages setup( name='django-waffle', version='0.20.0', description='A feature flipper for Django.', long_description=open('README.rst').read(), author='James Socol', author_email='me@jamessocol.com', url='http://github.com/django-waffle/django-waffle', license='BSD', packages=find_packages(exclude=['test_app', 'test_settings']), include_package_data=True, package_data={'': ['README.rst']}, zip_safe=False, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Django', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Framework :: Django', 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.0', 'Framework :: Django :: 3.0', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Topic :: Software Development :: Libraries :: Python Modules', ], install_requires=['six>=1.13.0'] ) django-waffle-0.20.0/test_app/000077500000000000000000000000001362207625100161205ustar00rootroot00000000000000django-waffle-0.20.0/test_app/__init__.py000066400000000000000000000000001362207625100202170ustar00rootroot00000000000000django-waffle-0.20.0/test_app/migrations/000077500000000000000000000000001362207625100202745ustar00rootroot00000000000000django-waffle-0.20.0/test_app/migrations/0001_initial.py000066400000000000000000000100361362207625100227370ustar00rootroot00000000000000# Generated by Django 2.0.4 on 2018-04-26 07:42 from django.conf import settings from django.db import migrations, models import django.db.models.deletion import django.utils.timezone class Migration(migrations.Migration): initial = True dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='Company', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100)), ], ), migrations.CreateModel( name='CompanyAwareFlag', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(help_text='The human/computer readable name.', max_length=100, unique=True, verbose_name='Name')), ('everyone', models.NullBooleanField(help_text='Flip this flag on (Yes) or off (No) for everyone, overriding all other settings. Leave as Unknown to use normally.', verbose_name='Everyone')), ('percent', models.DecimalField(blank=True, decimal_places=1, help_text='A number between 0.0 and 99.9 to indicate a percentage of users for whom this flag will be active.', max_digits=3, null=True, verbose_name='Percent')), ('testing', models.BooleanField(default=False, help_text='Allow this flag to be set for a session for user testing', verbose_name='Testing')), ('superusers', models.BooleanField(default=True, help_text='Flag always active for superusers?', verbose_name='Superusers')), ('staff', models.BooleanField(default=False, help_text='Flag always active for staff?', verbose_name='Staff')), ('authenticated', models.BooleanField(default=False, help_text='Flag always active for authenticated users?', verbose_name='Authenticated')), ('languages', models.TextField(blank=True, default='', help_text='Activate this flag for users with one of these languages (comma-separated list)', verbose_name='Languages')), ('rollout', models.BooleanField(default=False, help_text='Activate roll-out mode?', verbose_name='Rollout')), ('note', models.TextField(blank=True, help_text='Note where this Flag is used.', verbose_name='Note')), ('created', models.DateTimeField(db_index=True, default=django.utils.timezone.now, help_text='Date when this Flag was created.', verbose_name='Created')), ('modified', models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this Flag was last modified.', verbose_name='Modified')), ('companies', models.ManyToManyField(blank=True, help_text='Activate this flag for these companies.', to='test_app.Company')), ('groups', models.ManyToManyField(blank=True, help_text='Activate this flag for these user groups.', to='auth.Group', verbose_name='Groups')), ('users', models.ManyToManyField(blank=True, help_text='Activate this flag for these users.', to=settings.AUTH_USER_MODEL, verbose_name='Users')), ], options={ 'verbose_name': 'Flag', 'verbose_name_plural': 'Flags', 'abstract': False, }, ), migrations.CreateModel( name='CompanyUser', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('password', models.CharField(max_length=128, verbose_name='password')), ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('username', models.CharField(max_length=100)), ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_app.Company')), ], options={ 'abstract': False, }, ), ] django-waffle-0.20.0/test_app/migrations/__init__.py000066400000000000000000000000001362207625100223730ustar00rootroot00000000000000django-waffle-0.20.0/test_app/models.py000066400000000000000000000042731362207625100177630ustar00rootroot00000000000000from django.contrib.auth.base_user import AbstractBaseUser from django.db import models from django.db.models import CASCADE from django.utils.translation import ugettext_lazy as _ from waffle.models import AbstractUserFlag, CACHE_EMPTY from waffle.utils import get_setting, keyfmt, get_cache cache = get_cache() class Company(models.Model): name = models.CharField( max_length=100, ) class CompanyUser(AbstractBaseUser): company = models.ForeignKey( Company, on_delete=CASCADE ) username = models.CharField( max_length=100, ) class CompanyAwareFlag(AbstractUserFlag): FLAG_COMPANIES_CACHE_KEY = 'FLAG_COMPANIES_CACHE_KEY' FLAG_COMPANIES_CACHE_KEY_DEFAULT = 'flag:%s:companies' companies = models.ManyToManyField( Company, blank=True, help_text=_('Activate this flag for these companies.'), ) def get_flush_keys(self, flush_keys=None): flush_keys = super(CompanyAwareFlag, self).get_flush_keys(flush_keys) companies_cache_key = get_setting(CompanyAwareFlag.FLAG_COMPANIES_CACHE_KEY, CompanyAwareFlag.FLAG_COMPANIES_CACHE_KEY_DEFAULT) flush_keys.append(keyfmt(companies_cache_key, self.name)) return flush_keys def is_active_for_user(self, user): is_active = super(CompanyAwareFlag, self).is_active_for_user(user) if is_active: return is_active if getattr(user, 'company_id', None): company_ids = self._get_company_ids() if user.company_id in company_ids: return True def _get_company_ids(self): cache_key = keyfmt( get_setting(CompanyAwareFlag.FLAG_COMPANIES_CACHE_KEY, CompanyAwareFlag.FLAG_COMPANIES_CACHE_KEY_DEFAULT), self.name ) cached = cache.get(cache_key) if cached == CACHE_EMPTY: return set() if cached: return cached company_ids = set(self.companies.all().values_list('pk', flat=True)) if not company_ids: cache.add(cache_key, CACHE_EMPTY) return set() cache.add(cache_key, company_ids) return company_ids django-waffle-0.20.0/test_app/templates/000077500000000000000000000000001362207625100201165ustar00rootroot00000000000000django-waffle-0.20.0/test_app/templates/django/000077500000000000000000000000001362207625100213605ustar00rootroot00000000000000django-waffle-0.20.0/test_app/templates/django/django.html000066400000000000000000000007611362207625100235140ustar00rootroot00000000000000{% load waffle_tags %} {% flag flag %} flag on {% else %} flag off {% endflag %} {% switch switch %} switch on {% else %} switch off {% endswitch %} {% sample sample %} sample on {% else %} sample off {% endsample %} {% flag flag_var %} flag_var on {% else %} flag_var off {% endflag %} {% switch switch_var %} switch_var on {% else %} switch_var off {% endswitch %} {% sample sample_var %} sample_var on {% else %} sample_var off {% endsample %} {% wafflejs %} django-waffle-0.20.0/test_app/templates/django/django_email.html000066400000000000000000000002521362207625100246560ustar00rootroot00000000000000{% load waffle_tags %} {% switch switch %} switch on {% else %} switch off {% endswitch %} {% sample sample %} sample on {% else %} sample off {% endsample %} django-waffle-0.20.0/test_app/templates/jinja/000077500000000000000000000000001362207625100212115ustar00rootroot00000000000000django-waffle-0.20.0/test_app/templates/jinja/jinja.html000066400000000000000000000004061362207625100231720ustar00rootroot00000000000000{% if waffle.flag('flag') %} flag on {% else %} flag off {% endif %} {% if waffle.switch('switch') %} switch on {% else %} switch off {% endif %} {% if waffle.sample('sample') %} sample on {% else %} sample off {% endif %} {{ waffle.wafflejs() }} django-waffle-0.20.0/test_app/urls.py000066400000000000000000000037051362207625100174640ustar00rootroot00000000000000from django.conf.urls import url, include from django.contrib import admin from django.http import HttpResponseNotFound, HttpResponseServerError from test_app import views def handler404(r, exception=None): return HttpResponseNotFound() def handler500(r, exception=None): return HttpResponseServerError() admin.autodiscover() urlpatterns = [ url(r'^flag_in_view', views.flag_in_view, name='flag_in_view'), url(r'^switch-on', views.switched_view), url(r'^switch-off', views.switched_off_view), url(r'^flag-on', views.flagged_view), url(r'^foo_view', views.foo_view, name='foo_view'), url(r'^foo_view_with_args/(?P\d+)/', views.foo_view_with_args, name='foo_view_with_args'), url(r'^switched_view_with_valid_redirect', views.switched_view_with_valid_redirect), url(r'^switched_view_with_valid_url_name', views.switched_view_with_valid_url_name), url(r'^switched_view_with_args_with_valid_redirect/(?P\d+)/', views.switched_view_with_args_with_valid_redirect), url(r'^switched_view_with_args_with_valid_url_name/(?P\d+)/', views.switched_view_with_args_with_valid_url_name), url(r'^switched_view_with_invalid_redirect', views.switched_view_with_invalid_redirect), url(r'^flagged_view_with_valid_redirect', views.flagged_view_with_valid_redirect), url(r'^flagged_view_with_valid_url_name', views.flagged_view_with_valid_url_name), url(r'^flagged_view_with_args_with_valid_redirect/(?P\d+)/', views.flagged_view_with_args_with_valid_redirect), url(r'^flagged_view_with_args_with_valid_url_name/(?P\d+)/', views.flagged_view_with_args_with_valid_url_name), url(r'^flagged_view_with_invalid_redirect', views.flagged_view_with_invalid_redirect), url(r'^flag-off', views.flagged_off_view), url(r'^', include('waffle.urls')), url(r'^admin/', admin.site.urls), ] django-waffle-0.20.0/test_app/views.py000066400000000000000000000066361362207625100176420ustar00rootroot00000000000000from django.http import HttpResponse from django.shortcuts import render from django.template.loader import render_to_string from django.views.generic import View from waffle import flag_is_active from waffle.decorators import waffle_flag, waffle_switch from waffle.mixins import WaffleFlagMixin, WaffleSampleMixin, WaffleSwitchMixin def flag_in_view(request): if flag_is_active(request, 'myflag'): return HttpResponse('on') return HttpResponse('off') def flag_in_jinja(request): return render(request, 'jinja/jinja.html') def flag_in_django(request): context = { 'flag_var': 'flag_var', 'switch_var': 'switch_var', 'sample_var': 'sample_var', } return render(request, 'django/django.html', context=context) def no_request_context(request): return render_to_string('django/django_email.html', context={}) @waffle_switch('foo') def switched_view(request): return HttpResponse('foo') @waffle_switch('!foo') def switched_off_view(request): return HttpResponse('foo') @waffle_flag('foo') def flagged_view(request): return HttpResponse('foo') @waffle_flag('!foo') def flagged_off_view(request): return HttpResponse('foo') def foo_view(request): return HttpResponse('redirected') def foo_view_with_args(request, some_number): return HttpResponse('redirected with {}'.format(some_number)) @waffle_switch('foo', redirect_to=foo_view) def switched_view_with_valid_redirect(request): return HttpResponse('foo') @waffle_switch('foo', redirect_to='foo_view') def switched_view_with_valid_url_name(request): return HttpResponse('foo') @waffle_switch('foo', redirect_to=foo_view_with_args) def switched_view_with_args_with_valid_redirect(request, some_number): return HttpResponse('foo with {}'.format(some_number)) @waffle_switch('foo', redirect_to='foo_view_with_args') def switched_view_with_args_with_valid_url_name(request, some_number): return HttpResponse('foo with {}'.format(some_number)) @waffle_switch('foo', redirect_to='invalid_view') def switched_view_with_invalid_redirect(request): return HttpResponse('foo') @waffle_flag('foo', redirect_to=foo_view) def flagged_view_with_valid_redirect(request): return HttpResponse('foo') @waffle_flag('foo', redirect_to='foo_view') def flagged_view_with_valid_url_name(request): return HttpResponse('foo') @waffle_flag('foo', redirect_to=foo_view_with_args) def flagged_view_with_args_with_valid_redirect(request, some_number): return HttpResponse('foo with {}'.format(some_number)) @waffle_flag('foo', redirect_to='foo_view_with_args') def flagged_view_with_args_with_valid_url_name(request, some_number): return HttpResponse('foo with {}'.format(some_number)) @waffle_flag('foo', redirect_to='invalid_view') def flagged_view_with_invalid_redirect(request): return HttpResponse('foo') class BaseWaffleView(View): def get(self, request, *args, **kwargs): return HttpResponse('foo') class FlagView(WaffleFlagMixin, BaseWaffleView): waffle_flag = 'foo' class FlagOffView(WaffleFlagMixin, BaseWaffleView): waffle_flag = '!foo' class SampleView(WaffleSampleMixin, BaseWaffleView): waffle_sample = 'foo' class SampleOffView(WaffleSampleMixin, BaseWaffleView): waffle_sample = '!foo' class SwitchView(WaffleSwitchMixin, BaseWaffleView): waffle_switch = 'foo' class SwitchOffView(WaffleSwitchMixin, BaseWaffleView): waffle_switch = '!foo' django-waffle-0.20.0/test_settings.py000066400000000000000000000055071362207625100175610ustar00rootroot00000000000000import os try: import django_jinja JINJA_INSTALLED = True except ImportError: JINJA_INSTALLED = False # Make filepaths relative to settings. ROOT = os.path.dirname(os.path.abspath(__file__)) path = lambda *a: os.path.join(ROOT, *a) DEBUG = True TEST_RUNNER = 'django.test.runner.DiscoverRunner' JINJA_CONFIG = {} SITE_ID = 1 USE_I18N = False SECRET_KEY = 'foobar' DATABASES = { 'default': { 'NAME': 'test.db', 'ENGINE': 'django.db.backends.sqlite3', }, # Provide a readonly DB for testing DB replication scenarios. 'readonly': { 'NAME': 'test.readonly.db', 'ENGINE': 'django.db.backends.sqlite3', } } if 'DATABASE_URL' in os.environ: try: import dj_database_url import psycopg2 DATABASES['default'] = dj_database_url.config() except ImportError: raise ImportError('Using the DATABASE_URL variable requires ' 'dj-database-url and psycopg2. Try:\n\npip install ' '-r travis.txt') INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'waffle', 'test_app', ) MIDDLEWARE = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'waffle.middleware.WaffleMiddleware', ) ROOT_URLCONF = 'test_app.urls' _CONTEXT_PROCESSORS = ( 'django.contrib.auth.context_processors.auth', 'django.template.context_processors.request', 'django.contrib.messages.context_processors.messages', ) if JINJA_INSTALLED: TEMPLATES = [ { 'BACKEND': 'django_jinja.backend.Jinja2', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'match_regex': r'jinja.*', 'match_extension': '', 'newstyle_gettext': True, 'context_processors': _CONTEXT_PROCESSORS, 'undefined': 'jinja2.Undefined', 'extensions': [ 'jinja2.ext.i18n', 'jinja2.ext.autoescape', 'waffle.jinja.WaffleExtension', ], } } ] else: TEMPLATES = [] TEMPLATES.append( { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'debug': DEBUG, 'context_processors': _CONTEXT_PROCESSORS, } } ) WAFFLE_FLAG_DEFAULT = False WAFFLE_SWITCH_DEFAULT = False WAFFLE_SAMPLE_DEFAULT = False WAFFLE_READ_FROM_WRITE_DB = False WAFFLE_OVERRIDE = False WAFFLE_CACHE_PREFIX = 'test:' django-waffle-0.20.0/tox.ini000066400000000000000000000010611362207625100156120ustar00rootroot00000000000000[tox] envlist = py{35,36}-django111 py{35,36,37}-django20 py{35,36,37}-django21 py{35,36,37,38}-django22 py{36,37,38}-django30 [testenv] deps = django111: Django>=1.11,<2.0 django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 django22: Django>=2.2,<2.3 django30: Django>=3.0,<3.1 -rtravis.txt passenv = DATABASE_URL commands = ./run.sh test [testenv:i18n] deps = Django>=3.0,<3.1 -rtravis.txt commands = ./run.sh makemessages ./run.sh compilemessages ./run.sh find_uncommitted_translations django-waffle-0.20.0/travis.txt000066400000000000000000000001601362207625100163470ustar00rootroot00000000000000dj-database-url==0.5.0 django-jinja>=2.4.1,<3 Jinja2>=2.7.1 mock==1.3.0 psycopg2-binary>=2.7.7 transifex-client django-waffle-0.20.0/waffle/000077500000000000000000000000001362207625100155455ustar00rootroot00000000000000django-waffle-0.20.0/waffle/.flake8000066400000000000000000000005721362207625100167240ustar00rootroot00000000000000[flake8] ; Use a line length of 120 instead of pep8's default 79 max-line-length = 120 ; Files not checked: ; - .*: don't check 'hidden' directories ; - migrations: most of these are autogenerated and don't need a check ; - manage: these are autogenerated and don't need a check ; - docs: contains autogenerated code that doesn't need a check exclude = .?*,*migrations,docs django-waffle-0.20.0/waffle/__init__.py000077500000000000000000000027401362207625100176640ustar00rootroot00000000000000from __future__ import unicode_literals from django.core.exceptions import ImproperlyConfigured from waffle.utils import get_setting from django.apps import apps as django_apps VERSION = (0, 20, 0) __version__ = '.'.join(map(str, VERSION)) default_app_config = 'waffle.apps.WaffleConfig' def flag_is_active(request, flag_name): flag = get_waffle_flag_model().get(flag_name) return flag.is_active(request) def switch_is_active(switch_name): from .models import Switch switch = Switch.get(switch_name) return switch.is_active() def sample_is_active(sample_name): from .models import Sample sample = Sample.get(sample_name) return sample.is_active() def get_waffle_flag_model(): """ Returns the waffle Flag model that is active in this project. """ # Add backwards compatibility by not requiring adding of WAFFLE_FLAG_MODEL # for everyone who upgrades. # At some point it would be helpful to require this to be defined explicitly, # but no for now, to remove pain form upgrading. flag_model_name = get_setting('FLAG_MODEL', 'waffle.Flag') try: return django_apps.get_model(flag_model_name) except ValueError: raise ImproperlyConfigured("WAFFLE_FLAG_MODEL must be of the form 'app_label.model_name'") except LookupError: raise ImproperlyConfigured( "WAFFLE_FLAG_MODEL refers to model '{}' that has not been installed".format( flag_model_name ) ) django-waffle-0.20.0/waffle/admin.py000077500000000000000000000113021362207625100172070ustar00rootroot00000000000000from __future__ import unicode_literals import six from django.contrib import admin from django.contrib.admin.models import LogEntry, CHANGE, DELETION from django.contrib.admin.widgets import ManyToManyRawIdWidget from django.contrib.contenttypes.models import ContentType from django.utils.html import escape if six.PY2: from django.utils.translation import ugettext_lazy as _ else: from django.utils.translation import gettext_lazy as _ from waffle.models import Flag, Sample, Switch class BaseAdmin(admin.ModelAdmin): search_fields = ('name', 'note') def get_actions(self, request): actions = super(BaseAdmin, self).get_actions(request) if 'delete_selected' in actions: del actions['delete_selected'] return actions def _add_log_entry(user, model, description, action_flag): LogEntry.objects.create( user=user, content_type=ContentType.objects.get_for_model(type(model)), object_id=model.id, object_repr=model.name + " " + description, action_flag=action_flag ) def enable_for_all(ma, request, qs): # Iterate over all objects to cause cache invalidation. for f in qs.all(): _add_log_entry(request.user, f, "on", CHANGE) f.everyone = True f.save() def disable_for_all(ma, request, qs): # Iterate over all objects to cause cache invalidation. for f in qs.all(): _add_log_entry(request.user, f, "off", CHANGE) f.everyone = False f.save() def delete_individually(ma, request, qs): # Iterate over all objects to cause cache invalidation. for f in qs.all(): _add_log_entry(request.user, f, "deleted", DELETION) f.delete() enable_for_all.short_description = _('Enable selected flags for everyone') disable_for_all.short_description = _('Disable selected flags for everyone') delete_individually.short_description = _('Delete selected') enable_for_all.allowed_permissions = ('change',) disable_for_all.allowed_permissions = ('change',) delete_individually.allowed_permissions = ('delete',) class InformativeManyToManyRawIdWidget(ManyToManyRawIdWidget): """Widget for ManyToManyField to Users. Will display the names of the users in a parenthesised list after the input field. This widget works with all models that have a "name" field. """ def label_and_url_for_value(self, values): names = [] key = self.rel.get_related_field().name for value in values: try: name = self.rel.model._default_manager \ .using(self.db) \ .get(**{key: value}) names.append(escape(str(name))) except self.rel.model.DoesNotExist: names.append('') return "(" + ", ".join(names) + ")", "" class FlagAdmin(BaseAdmin): actions = [enable_for_all, disable_for_all, delete_individually] list_display = ('name', 'note', 'everyone', 'percent', 'superusers', 'staff', 'authenticated', 'languages') list_filter = ('everyone', 'superusers', 'staff', 'authenticated') raw_id_fields = ('users', ) ordering = ('-id',) def formfield_for_dbfield(self, db_field, **kwargs): if db_field.name == 'users': kwargs.pop('request', None) kwargs['widget'] = \ InformativeManyToManyRawIdWidget(db_field.remote_field, self.admin_site, using=kwargs.get("using")) return db_field.formfield(**kwargs) return super(FlagAdmin, self).formfield_for_dbfield(db_field, **kwargs) def enable_switches(ma, request, qs): for switch in qs: _add_log_entry(request.user, switch, "on", CHANGE) switch.active = True switch.save() def disable_switches(ma, request, qs): for switch in qs: _add_log_entry(request.user, switch, "off", CHANGE) switch.active = False switch.save() enable_switches.short_description = _('Enable selected switches') disable_switches.short_description = _('Disable selected switches') enable_switches.allowed_permissions = ('change',) disable_switches.allowed_permissions = ('change',) class SwitchAdmin(BaseAdmin): actions = [enable_switches, disable_switches, delete_individually] list_display = ('name', 'active', 'note', 'created', 'modified') list_filter = ('active',) ordering = ('-id',) class SampleAdmin(BaseAdmin): actions = [delete_individually] list_display = ('name', 'percent', 'note', 'created', 'modified') ordering = ('-id',) admin.site.register(Flag, FlagAdmin) admin.site.register(Sample, SampleAdmin) admin.site.register(Switch, SwitchAdmin) django-waffle-0.20.0/waffle/apps.py000066400000000000000000000002741362207625100170650ustar00rootroot00000000000000from django.apps import AppConfig class WaffleConfig(AppConfig): name = 'waffle' verbose_name = 'django-waffle' def ready(self): import waffle.signals # noqa: F401 django-waffle-0.20.0/waffle/decorators.py000066400000000000000000000036201362207625100202650ustar00rootroot00000000000000from __future__ import unicode_literals from functools import wraps, WRAPPER_ASSIGNMENTS from django.http import Http404 from django.shortcuts import redirect from django.urls import reverse, NoReverseMatch from waffle import flag_is_active, switch_is_active def waffle_flag(flag_name, redirect_to=None): def decorator(view): @wraps(view, assigned=WRAPPER_ASSIGNMENTS) def _wrapped_view(request, *args, **kwargs): if flag_name.startswith('!'): active = not flag_is_active(request, flag_name[1:]) else: active = flag_is_active(request, flag_name) if not active: response_to_redirect_to = get_response_to_redirect(redirect_to, *args, **kwargs) if response_to_redirect_to: return response_to_redirect_to else: raise Http404 return view(request, *args, **kwargs) return _wrapped_view return decorator def waffle_switch(switch_name, redirect_to=None): def decorator(view): @wraps(view, assigned=WRAPPER_ASSIGNMENTS) def _wrapped_view(request, *args, **kwargs): if switch_name.startswith('!'): active = not switch_is_active(switch_name[1:]) else: active = switch_is_active(switch_name) if not active: response_to_redirect_to = get_response_to_redirect(redirect_to, *args, **kwargs) if response_to_redirect_to: return response_to_redirect_to else: raise Http404 return view(request, *args, **kwargs) return _wrapped_view return decorator def get_response_to_redirect(view, *args, **kwargs): try: return redirect(reverse(view, args=args, kwargs=kwargs)) if view else None except NoReverseMatch: return None django-waffle-0.20.0/waffle/defaults.py000066400000000000000000000013641362207625100177320ustar00rootroot00000000000000from __future__ import unicode_literals COOKIE = 'dwf_%s' TEST_COOKIE = 'dwft_%s' SECURE = True MAX_AGE = 2592000 # 1 month in seconds CACHE_PREFIX = 'waffle:' CACHE_NAME = 'default' FLAG_CACHE_KEY = 'flag:%s' FLAG_USERS_CACHE_KEY = 'flag:%s:users' FLAG_GROUPS_CACHE_KEY = 'flag:%s:groups' ALL_FLAGS_CACHE_KEY = 'flags:all' SAMPLE_CACHE_KEY = 'sample:%s' ALL_SAMPLES_CACHE_KEY = 'samples:all' SWITCH_CACHE_KEY = 'switch:%s' ALL_SWITCHES_CACHE_KEY = 'switches:all' FLAG_DEFAULT = False SAMPLE_DEFAULT = False SWITCH_DEFAULT = False READ_FROM_WRITE_DB = False CREATE_MISSING_FLAGS = False CREATE_MISSING_SAMPLES = False CREATE_MISSING_SWITCHES = False LOG_MISSING_FLAGS = None LOG_MISSING_SAMPLES = None LOG_MISSING_SWITCHES = None OVERRIDE = False django-waffle-0.20.0/waffle/jinja.py000066400000000000000000000013231362207625100172110ustar00rootroot00000000000000from __future__ import unicode_literals import jinja2 from jinja2.ext import Extension from waffle import flag_is_active, sample_is_active, switch_is_active from waffle.views import _generate_waffle_js @jinja2.contextfunction def flag_helper(context, flag_name): return flag_is_active(context['request'], flag_name) @jinja2.contextfunction def inline_wafflejs_helper(context): return _generate_waffle_js(context['request']) class WaffleExtension(Extension): def __init__(self, environment): environment.globals['waffle'] = { 'flag': flag_helper, 'switch': switch_is_active, 'sample': sample_is_active, 'wafflejs': inline_wafflejs_helper } django-waffle-0.20.0/waffle/locale/000077500000000000000000000000001362207625100170045ustar00rootroot00000000000000django-waffle-0.20.0/waffle/locale/en_US/000077500000000000000000000000001362207625100200155ustar00rootroot00000000000000django-waffle-0.20.0/waffle/locale/en_US/LC_MESSAGES/000077500000000000000000000000001362207625100216025ustar00rootroot00000000000000django-waffle-0.20.0/waffle/locale/en_US/LC_MESSAGES/django.mo000066400000000000000000000005721362207625100234050ustar00rootroot00000000000000$,8@9Project-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2018-03-05 20:22-0800 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: FULL NAME Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit django-waffle-0.20.0/waffle/locale/en_US/LC_MESSAGES/django.po000066400000000000000000000074321362207625100234120ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-26 14:53+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: admin.py:39 msgid "Enable selected flags for everyone" msgstr "" #: admin.py:40 msgid "Disable selected flags for everyone" msgstr "" #: admin.py:41 msgid "Delete selected" msgstr "" #: admin.py:65 msgid "Enable selected switches" msgstr "" #: admin.py:66 msgid "Disable selected switches" msgstr "" #: models.py:126 models.py:389 models.py:440 msgid "The human/computer readable name." msgstr "" #: models.py:127 models.py:390 models.py:441 msgid "Name" msgstr "" #: models.py:132 msgid "" "Flip this flag on (Yes) or off (No) for everyone, overriding all other " "settings. Leave as Unknown to use normally." msgstr "" #: models.py:134 msgid "Everyone" msgstr "" #: models.py:141 msgid "" "A number between 0.0 and 99.9 to indicate a percentage of users for whom " "this flag will be active." msgstr "" #: models.py:143 models.py:448 msgid "Percent" msgstr "" #: models.py:147 msgid "Allow this flag to be set for a session for user testing" msgstr "" #: models.py:148 msgid "Testing" msgstr "" #: models.py:152 msgid "Flag always active for superusers?" msgstr "" #: models.py:153 msgid "Superusers" msgstr "" #: models.py:157 msgid "Flag always active for staff?" msgstr "" #: models.py:158 msgid "Staff" msgstr "" #: models.py:162 msgid "Flag always active for authenticated users?" msgstr "" #: models.py:163 msgid "Authenticated" msgstr "" #: models.py:168 msgid "" "Activate this flag for users with one of these languages (comma-separated " "list)" msgstr "" #: models.py:169 msgid "Languages" msgstr "" #: models.py:173 msgid "Activate roll-out mode?" msgstr "" #: models.py:174 msgid "Rollout" msgstr "" #: models.py:178 msgid "Note where this Flag is used." msgstr "" #: models.py:179 models.py:400 models.py:453 msgid "Note" msgstr "" #: models.py:184 msgid "Date when this Flag was created." msgstr "" #: models.py:185 models.py:406 models.py:459 msgid "Created" msgstr "" #: models.py:189 msgid "Date when this Flag was last modified." msgstr "" #: models.py:190 models.py:411 models.py:464 msgid "Modified" msgstr "" #: models.py:200 models.py:306 models.py:375 msgid "Flag" msgstr "" #: models.py:201 models.py:307 models.py:376 msgid "Flags" msgstr "" #: models.py:294 msgid "Activate this flag for these user groups." msgstr "" #: models.py:295 msgid "Groups" msgstr "" #: models.py:300 msgid "Activate this flag for these users." msgstr "" #: models.py:301 msgid "Users" msgstr "" #: models.py:394 msgid "Is this switch active?" msgstr "" #: models.py:395 msgid "Active" msgstr "" #: models.py:399 msgid "Note where this Switch is used." msgstr "" #: models.py:405 msgid "Date when this Switch was created." msgstr "" #: models.py:410 msgid "Date when this Switch was last modified." msgstr "" #: models.py:420 msgid "Switch" msgstr "" #: models.py:421 msgid "Switches" msgstr "" #: models.py:446 msgid "" "A number between 0.0 and 100.0 to indicate a percentage of the time this " "sample will be active." msgstr "" #: models.py:452 msgid "Note where this Sample is used." msgstr "" #: models.py:458 msgid "Date when this Sample was created." msgstr "" #: models.py:463 msgid "Date when this Sample was last modified." msgstr "" #: models.py:473 msgid "Sample" msgstr "" #: models.py:474 msgid "Samples" msgstr "" django-waffle-0.20.0/waffle/locale/ru/000077500000000000000000000000001362207625100174325ustar00rootroot00000000000000django-waffle-0.20.0/waffle/locale/ru/LC_MESSAGES/000077500000000000000000000000001362207625100212175ustar00rootroot00000000000000django-waffle-0.20.0/waffle/locale/ru/LC_MESSAGES/django.mo000066400000000000000000000125451362207625100230250ustar00rootroot00000000000000.=_bQ)#Oj8q  &"(+"T(w+"5Xr^  *Jjrz !3 3w _ N Z  h?n 0G.4Ec6M7.5f.n?CQ    )4CJR_RX^m  "#6L#% "+  &(,*. ' $)- !A number between 0.0 and 100.0 to indicate a percentage of the time this sample will be active.A number between 0.0 and 99.9 to indicate a percentage of users for whom this flag will be active.Activate roll-out mode?Activate this flag for these user groups.Activate this flag for these users.Activate this flag for users with one of these languages (comma-separated list)ActiveAllow this flag to be set for a session for user testingAuthenticatedCreatedDate when this Flag was created.Date when this Flag was last modified.Date when this Sample was created.Date when this Sample was last modified.Date when this Switch was created.Date when this Switch was last modified.Disable the selected switches.Enable the selected switches.EveryoneFlagFlag always active for authenticated users?Flag always active for staff?Flag always active for superusers?FlagsFlip this flag on (Yes) or off (No) for everyone, overriding all other settings. Leave as Unknown to use normally.GroupsIs this switch active?LanguagesModifiedNameNoteNote where this Flag is used.Note where this Sample is used.Note where this Switch is used.PercentRolloutSampleSamplesStaffSuperusersSwitchSwitchesTestingThe human/computer readable name.UsersProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2018-03-05 20:22-0800 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: Kostya Esmukov , 2018 Language-Team: Russian (https://www.transifex.com/django-waffle/teams/84077/ru/) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Language: ru Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3); Число в интервале от 0.0 до 100.0, указывающее процент проверок, в который эта Проба будет активной.Число в интервале от 0.0 до 99.9, указывающее процент пользователей, для которых этот Флаг будет активен.Активировать режим выкатки?Включить этот Флаг для этих пользовательских групп.Включить этот Флаг для этих пользователей.Включить этот Флаг для пользователей с одним из перечисленных (через запятую) языковВключёнРазрешить включение этого Флага со стороны пользователяАутентифицированные пользователиСозданДата создания этого Флага.Дата последнего изменения этого Флага.Дата создания этой Пробы.Дата последнего изменения этой Пробы.Дата создания этого Тумблера.Дата последнего изменения этого Тумблера.Выключить выбранные Тумблеры.Включить выбранные Тумблеры.Переопределение для всехФлагФлаг всегда активен для аутентифицированных пользователей?Флаг всегда активен для персонала?Флаг всегда активен для суперпользователей?ФлагиВключить (Да) или Выключить (Нет) Флаг для всех, перекрывая все остальные настройки. Оставьте как Неизвестно, чтобы использовать обычный режим работы Флага.ГруппыТумблер включен?ЯзыкиИзменёнИмяПримечаниеПримечание о том, где используется этот Флаг.Примечание о том, где используется эта Проба.Примечание о том, где используется этот Тумблер.ПроцентРежим выкаткиПробаПробыПерсоналСуперпользователиТумблерТумблерыРежим тестированияЧеловеко-/машиночитаемое имя.Пользователиdjango-waffle-0.20.0/waffle/locale/ru/LC_MESSAGES/django.po000066400000000000000000000153731362207625100230320ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-04-26 14:53+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Kostya Esmukov , 2018\n" "Language-Team: Russian (https://www.transifex.com/django-waffle/teams/84077/" "ru/)\n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" "%100>=11 && n%100<=14)? 2 : 3);\n" #: admin.py:39 msgid "Enable selected flags for everyone" msgstr "Включить выбранные Флаги для всех" #: admin.py:40 msgid "Disable selected flags for everyone" msgstr "Выключить выбранные Флаги для всех" #: admin.py:41 msgid "Delete selected" msgstr "Удалить выбранные" #: admin.py:65 msgid "Enable selected switches" msgstr "Включить выбранные Тумблеры" #: admin.py:66 msgid "Disable selected switches" msgstr "Выключить выбранные Тумблеры" #: models.py:126 models.py:389 models.py:440 msgid "The human/computer readable name." msgstr "Человеко-/машиночитаемое имя" #: models.py:127 models.py:390 models.py:441 msgid "Name" msgstr "Имя" #: models.py:132 msgid "" "Flip this flag on (Yes) or off (No) for everyone, overriding all other " "settings. Leave as Unknown to use normally." msgstr "" "Включить (Да) или Выключить (Нет) Флаг для всех, перекрывая все остальные " "настройки. Оставьте как Неизвестно, чтобы использовать обычный режим работы " "Флага." #: models.py:134 msgid "Everyone" msgstr "Переопределение для всех" #: models.py:141 msgid "" "A number between 0.0 and 99.9 to indicate a percentage of users for whom " "this flag will be active." msgstr "" "Число в интервале от 0.0 до 99.9, указывающее процент пользователей, для " "которых этот Флаг будет активен." #: models.py:143 models.py:448 msgid "Percent" msgstr "Процент" #: models.py:147 msgid "Allow this flag to be set for a session for user testing" msgstr "Разрешить включение этого Флага со стороны пользователя" #: models.py:148 msgid "Testing" msgstr "Режим тестирования" #: models.py:152 msgid "Flag always active for superusers?" msgstr "Флаг всегда активен для суперпользователей?" #: models.py:153 msgid "Superusers" msgstr "Суперпользователи" #: models.py:157 msgid "Flag always active for staff?" msgstr "Флаг всегда активен для персонала?" #: models.py:158 msgid "Staff" msgstr "Персонал" #: models.py:162 msgid "Flag always active for authenticated users?" msgstr "Флаг всегда активен для аутентифицированных пользователей?" #: models.py:163 msgid "Authenticated" msgstr "Аутентифицированные пользователи" #: models.py:168 msgid "" "Activate this flag for users with one of these languages (comma-separated " "list)" msgstr "" "Включить этот Флаг для пользователей с одним из перечисленных (через " "запятую) языков" #: models.py:169 msgid "Languages" msgstr "Языки" #: models.py:173 msgid "Activate roll-out mode?" msgstr "Активировать режим выкатки?" #: models.py:174 msgid "Rollout" msgstr "Режим выкатки" #: models.py:178 msgid "Note where this Flag is used." msgstr "Примечание о том, где используется этот Флаг." #: models.py:179 models.py:400 models.py:453 msgid "Note" msgstr "Примечание" #: models.py:184 msgid "Date when this Flag was created." msgstr "Дата создания этого Флага." #: models.py:185 models.py:406 models.py:459 msgid "Created" msgstr "Создан" #: models.py:189 msgid "Date when this Flag was last modified." msgstr "Дата последнего изменения этого Флага." #: models.py:190 models.py:411 models.py:464 msgid "Modified" msgstr "Изменён" #: models.py:200 models.py:306 models.py:375 msgid "Flag" msgstr "Флаг" #: models.py:201 models.py:307 models.py:376 msgid "Flags" msgstr "Флаги" #: models.py:294 msgid "Activate this flag for these user groups." msgstr "Включить этот Флаг для этих пользовательских групп." #: models.py:295 msgid "Groups" msgstr "Группы" #: models.py:300 msgid "Activate this flag for these users." msgstr "Включить этот Флаг для этих пользователей." #: models.py:301 msgid "Users" msgstr "Пользователи" #: models.py:394 msgid "Is this switch active?" msgstr "Тумблер включен?" #: models.py:395 msgid "Active" msgstr "Включён" #: models.py:399 msgid "Note where this Switch is used." msgstr "Примечание о том, где используется этот Тумблер." #: models.py:405 msgid "Date when this Switch was created." msgstr "Дата создания этого Тумблера." #: models.py:410 msgid "Date when this Switch was last modified." msgstr "Дата последнего изменения этого Тумблера." #: models.py:420 msgid "Switch" msgstr "Тумблер" #: models.py:421 msgid "Switches" msgstr "Тумблеры" #: models.py:446 msgid "" "A number between 0.0 and 100.0 to indicate a percentage of the time this " "sample will be active." msgstr "" "Число в интервале от 0.0 до 100.0, указывающее процент проверок, в который " "эта Проба будет активной." #: models.py:452 msgid "Note where this Sample is used." msgstr "Примечание о том, где используется эта Проба." #: models.py:458 msgid "Date when this Sample was created." msgstr "Дата создания этой Пробы." #: models.py:463 msgid "Date when this Sample was last modified." msgstr "Дата последнего изменения этой Пробы." #: models.py:473 msgid "Sample" msgstr "Проба" #: models.py:474 msgid "Samples" msgstr "Пробы" django-waffle-0.20.0/waffle/management/000077500000000000000000000000001362207625100176615ustar00rootroot00000000000000django-waffle-0.20.0/waffle/management/__init__.py000066400000000000000000000000001362207625100217600ustar00rootroot00000000000000django-waffle-0.20.0/waffle/management/commands/000077500000000000000000000000001362207625100214625ustar00rootroot00000000000000django-waffle-0.20.0/waffle/management/commands/__init__.py000066400000000000000000000000001362207625100235610ustar00rootroot00000000000000django-waffle-0.20.0/waffle/management/commands/waffle_delete.py000066400000000000000000000033651362207625100246310ustar00rootroot00000000000000from __future__ import print_function from django.core.management.base import BaseCommand from waffle import get_waffle_flag_model from waffle.models import Sample, Switch class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( '--flags', action='store', dest='flag_names', nargs='*', help='List of flag names to delete.') parser.add_argument( '--samples', action='store', dest='sample_names', nargs='*', help='List of sample names to delete.') parser.add_argument( '--switches', action='store', dest='switch_names', nargs='*', help='List of switch names to delete.') help = 'Delete flags, samples, and switches from database' def handle(self, *args, **options): flags = options['flag_names'] if flags: flag_queryset = get_waffle_flag_model().objects.filter(name__in=flags) flag_count = flag_queryset.count() flag_queryset.delete() self.stdout.write('Deleted %s Flags' % flag_count) switches = options['switch_names'] if switches: switches_queryset = Switch.objects.filter(name__in=switches) switch_count = switches_queryset.count() switches_queryset.delete() self.stdout.write('Deleted %s Switches' % switch_count) samples = options['sample_names'] if samples: sample_queryset = Sample.objects.filter(name__in=samples) sample_count = sample_queryset.count() sample_queryset.delete() self.stdout.write('Deleted %s Samples' % sample_count) django-waffle-0.20.0/waffle/management/commands/waffle_flag.py000066400000000000000000000157441362207625100243040ustar00rootroot00000000000000from __future__ import print_function from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.core.management.base import BaseCommand, CommandError from django.db.models import Q from waffle import get_waffle_flag_model UserModel = get_user_model() class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( 'name', nargs='?', help='The name of the flag.') parser.add_argument( '-l', '--list', action='store_true', dest='list_flags', default=False, help='List existing samples.') parser.add_argument( '--everyone', action='store_true', dest='everyone', help='Activate flag for all users.') parser.add_argument( '--deactivate', action='store_false', dest='everyone', help='Deactivate flag for all users.') parser.add_argument( '--percent', '-p', action='store', type=int, dest='percent', help='Roll out the flag for a certain percentage of users. Takes ' 'a number between 0.0 and 100.0') parser.add_argument( '--superusers', action='store_true', dest='superusers', default=False, help='Turn on the flag for Django superusers.') parser.add_argument( '--staff', action='store_true', dest='staff', default=False, help='Turn on the flag for Django staff.') parser.add_argument( '--authenticated', action='store_true', dest='authenticated', default=False, help='Turn on the flag for logged in users.') parser.add_argument( '--group', '-g', action='append', default=list(), help='Turn on the flag for listed group names (use flag more ' 'than once for multiple groups). WARNING: This will remove ' 'any currently associated groups unless --append is used!') parser.add_argument( '--user', '-u', action='append', default=list(), help='Turn on the flag for listed usernames (use flag more ' 'than once for multiple users). WARNING: This will remove ' 'any currently associated users unless --append is used!') parser.add_argument( '--append', action='store_true', dest='append', default=False, help='Append only mode when adding groups.') parser.add_argument( '--rollout', '-r', action='store_true', dest='rollout', default=False, help='Turn on rollout mode.') parser.add_argument( '--create', action='store_true', dest='create', default=False, help='If the flag doesn\'t exist, create it.') parser.set_defaults(everyone=None) help = 'Modify a flag.' def handle(self, *args, **options): if options['list_flags']: self.stdout.write('Flags:') for flag in get_waffle_flag_model().objects.iterator(): self.stdout.write('NAME: %s' % flag.name) self.stdout.write('SUPERUSERS: %s' % flag.superusers) self.stdout.write('EVERYONE: %s' % flag.everyone) self.stdout.write('AUTHENTICATED: %s' % flag.authenticated) self.stdout.write('PERCENT: %s' % flag.percent) self.stdout.write('TESTING: %s' % flag.testing) self.stdout.write('ROLLOUT: %s' % flag.rollout) self.stdout.write('STAFF: %s' % flag.staff) self.stdout.write('GROUPS: %s' % list( flag.groups.values_list('name', flat=True)) ) self.stdout.write('USERS: %s' % list( flag.users.values_list(UserModel.USERNAME_FIELD, flat=True)) ) self.stdout.write('') return flag_name = options['name'] if not flag_name: raise CommandError('You need to specify a flag name.') if options['create']: flag, created = get_waffle_flag_model().objects.get_or_create(name=flag_name) if created: self.stdout.write('Creating flag: %s' % flag_name) else: try: flag = get_waffle_flag_model().objects.get(name=flag_name) except get_waffle_flag_model().DoesNotExist: raise CommandError('This flag does not exist.') # Loop through all options, setting Flag attributes that # match (ie. don't want to try setting flag.verbosity) for option in options: # Group isn't an attribute on the Flag, but a related Many to Many # field, so we handle it a bit differently by looking up groups and # adding each group to the flag individually if option == 'group': group_hash = {} for group in options['group']: try: group_instance = Group.objects.get(name=group) group_hash[group_instance.name] = group_instance.id except Group.DoesNotExist: raise CommandError('Group %s does not exist' % group) # If 'append' was not passed, we clear related groups if not options['append']: flag.groups.clear() self.stdout.write('Setting group(s): %s' % ( [name for name, _id in group_hash.items()]) ) for group_name, group_id in group_hash.items(): flag.groups.add(group_id) elif option == 'user': user_hash = set() for username in options['user']: try: user_instance = UserModel.objects.get( Q(**{UserModel.USERNAME_FIELD: username}) | Q(**{UserModel.EMAIL_FIELD: username}) ) user_hash.add(user_instance) except UserModel.DoesNotExist: raise CommandError('User %s does not exist' % username) # If 'append' was not passed, we clear related users if not options['append']: flag.users.clear() self.stdout.write('Setting user(s): %s' % user_hash) # for user in user_hash: flag.users.add(*[user.id for user in user_hash]) elif hasattr(flag, option): self.stdout.write('Setting %s: %s' % (option, options[option])) setattr(flag, option, options[option]) flag.save() django-waffle-0.20.0/waffle/management/commands/waffle_sample.py000066400000000000000000000042101362207625100246360ustar00rootroot00000000000000from __future__ import print_function from django.core.management.base import BaseCommand, CommandError from waffle.models import Sample class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( 'name', nargs='?', help='The name of the sample.') parser.add_argument( 'percent', nargs='?', type=int, help='The percentage of the time this sample will be active.') parser.add_argument( '-l', '--list', action='store_true', dest='list_samples', default=False, help='List existing samples.') parser.add_argument( '--create', action='store_true', dest='create', default=False, help='If the sample does not exist, create it.') help = 'Change percentage of a sample.' def handle(self, *args, **options): if options['list_samples']: self.stdout.write('Samples:') for sample in Sample.objects.iterator(): self.stdout.write('%s: %.1f%%' % (sample.name, sample.percent)) self.stdout.write('') return sample_name = options['name'] percent = options['percent'] if not (sample_name and percent): raise CommandError( 'You need to specify a sample name and percentage.' ) try: percent = float(percent) if not (0.0 <= percent <= 100.0): raise ValueError() except ValueError: raise CommandError('You need to enter a valid percentage value.') if options['create']: sample, created = Sample.objects.get_or_create( name=sample_name, defaults={'percent': 0}) if created: self.stdout.write('Creating sample: %s' % sample_name) else: try: sample = Sample.objects.get(name=sample_name) except Sample.DoesNotExist: raise CommandError('This sample does not exist.') sample.percent = percent sample.save() django-waffle-0.20.0/waffle/management/commands/waffle_switch.py000066400000000000000000000041531362207625100246640ustar00rootroot00000000000000from argparse import ArgumentTypeError from django.core.management.base import BaseCommand, CommandError from waffle.models import Switch def on_off_bool(string): if string not in ['on', 'off']: raise ArgumentTypeError("invalid choice: %r (choose from 'on', " "'off')" % string) return string == 'on' class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( 'name', nargs='?', help='The name of the switch.') parser.add_argument( 'state', nargs='?', type=on_off_bool, help='The state of the switch: on or off.') parser.add_argument( '-l', '--list', action='store_true', dest='list_switches', default=False, help='List existing switches.') parser.add_argument( '--create', action='store_true', dest='create', default=False, help='If the switch does not exist, create it.') help = 'Activate or deactivate a switch.' def handle(self, *args, **options): if options['list_switches']: self.stdout.write('Switches:') for switch in Switch.objects.iterator(): self.stdout.write( '%s: %s' % (switch.name, 'on' if switch.active else 'off') ) self.stdout.write('') return switch_name = options['name'] state = options['state'] if not (switch_name and state is not None): raise CommandError('You need to specify a switch name and state.') if options['create']: switch, created = Switch.objects.get_or_create(name=switch_name) if created: self.stdout.write('Creating switch: %s' % switch_name) else: try: switch = Switch.objects.get(name=switch_name) except Switch.DoesNotExist: raise CommandError('This switch does not exist.') switch.active = state switch.save() django-waffle-0.20.0/waffle/managers.py000066400000000000000000000013351362207625100177160ustar00rootroot00000000000000from __future__ import absolute_import, unicode_literals from django.db import models from waffle.utils import get_setting, get_cache class BaseManager(models.Manager): KEY_SETTING = '' def get_by_natural_key(self, name): return self.get(name=name) def create(self, *args, **kwargs): cache = get_cache() ret = super(BaseManager, self).create(*args, **kwargs) cache_key = get_setting(self.KEY_SETTING) cache.delete(cache_key) return ret class FlagManager(BaseManager): KEY_SETTING = 'ALL_FLAGS_CACHE_KEY' class SwitchManager(BaseManager): KEY_SETTING = 'ALL_SWITCHES_CACHE_KEY' class SampleManager(BaseManager): KEY_SETTING = 'ALL_SAMPLES_CACHE_KEY' django-waffle-0.20.0/waffle/middleware.py000066400000000000000000000021731362207625100202370ustar00rootroot00000000000000from __future__ import unicode_literals from django.utils.deprecation import MiddlewareMixin from django.utils.encoding import smart_str from waffle.utils import get_setting class WaffleMiddleware(MiddlewareMixin): def process_response(self, request, response): secure = get_setting('SECURE') max_age = get_setting('MAX_AGE') if hasattr(request, 'waffles'): for k in request.waffles: name = smart_str(get_setting('COOKIE') % k) active, rollout = request.waffles[k] if rollout and not active: # "Inactive" is a session cookie during rollout mode. age = None else: age = max_age response.set_cookie(name, value=active, max_age=age, secure=secure) if hasattr(request, 'waffle_tests'): for k in request.waffle_tests: name = smart_str(get_setting('TEST_COOKIE') % k) value = request.waffle_tests[k] response.set_cookie(name, value=value) return response django-waffle-0.20.0/waffle/migrations/000077500000000000000000000000001362207625100177215ustar00rootroot00000000000000django-waffle-0.20.0/waffle/migrations/0001_initial.py000066400000000000000000000106711362207625100223710ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations import django.utils.timezone from django.conf import settings class Migration(migrations.Migration): dependencies = [ ('auth', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='Flag', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(help_text='The human/computer readable name.', unique=True, max_length=100)), ('everyone', models.NullBooleanField(help_text='Flip this flag on (Yes) or off (No) for everyone, overriding all other settings. Leave as Unknown to use normally.')), ('percent', models.DecimalField(help_text='A number between 0.0 and 99.9 to indicate a percentage of users for whom this flag will be active.', null=True, max_digits=3, decimal_places=1, blank=True)), ('testing', models.BooleanField(default=False, help_text='Allow this flag to be set for a session for user testing.')), ('superusers', models.BooleanField(default=True, help_text='Flag always active for superusers?')), ('staff', models.BooleanField(default=False, help_text='Flag always active for staff?')), ('authenticated', models.BooleanField(default=False, help_text='Flag always active for authenticate users?')), ('languages', models.TextField(default='', help_text='Activate this flag for users with one of these languages (comma separated list)', blank=True)), ('rollout', models.BooleanField(default=False, help_text='Activate roll-out mode?')), ('note', models.TextField(help_text='Note where this Flag is used.', blank=True)), ('created', models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this Flag was created.', db_index=True)), ('modified', models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this Flag was last modified.')), ('groups', models.ManyToManyField(help_text='Activate this flag for these user groups.', to='auth.Group', blank=True)), ('users', models.ManyToManyField(help_text='Activate this flag for these users.', to=settings.AUTH_USER_MODEL, blank=True)), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='Sample', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(help_text='The human/computer readable name.', unique=True, max_length=100)), ('percent', models.DecimalField(help_text='A number between 0.0 and 100.0 to indicate a percentage of the time this sample will be active.', max_digits=4, decimal_places=1)), ('note', models.TextField(help_text='Note where this Sample is used.', blank=True)), ('created', models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this Sample was created.', db_index=True)), ('modified', models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this Sample was last modified.')), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='Switch', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(help_text='The human/computer readable name.', unique=True, max_length=100)), ('active', models.BooleanField(default=False, help_text='Is this flag active?')), ('note', models.TextField(help_text='Note where this Switch is used.', blank=True)), ('created', models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this Switch was created.', db_index=True)), ('modified', models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this Switch was last modified.')), ], options={ 'verbose_name_plural': 'Switches', }, bases=(models.Model,), ), ] django-waffle-0.20.0/waffle/migrations/0002_auto_20161201_0958.py000066400000000000000000000007361362207625100233530ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.10.3 on 2016-12-01 09:58 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('waffle', '0001_initial'), ] operations = [ migrations.AlterField( model_name='switch', name='active', field=models.BooleanField(default=False, help_text='Is this switch active?'), ), ] django-waffle-0.20.0/waffle/migrations/0003_update_strings_for_i18n.py000066400000000000000000000151431362207625100255010ustar00rootroot00000000000000# Generated by Django 2.0 on 2018-03-05 22:21 from django.conf import settings from django.db import migrations, models import django.utils.timezone class Migration(migrations.Migration): dependencies = [ ('waffle', '0002_auto_20161201_0958'), ] operations = [ migrations.AlterModelOptions( name='flag', options={'verbose_name': 'Flag', 'verbose_name_plural': 'Flags'}, ), migrations.AlterModelOptions( name='sample', options={'verbose_name': 'Sample', 'verbose_name_plural': 'Samples'}, ), migrations.AlterModelOptions( name='switch', options={'verbose_name': 'Switch', 'verbose_name_plural': 'Switches'}, ), migrations.AlterField( model_name='flag', name='authenticated', field=models.BooleanField(default=False, help_text='Flag always active for authenticated users?', verbose_name='Authenticated'), ), migrations.AlterField( model_name='flag', name='created', field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, help_text='Date when this Flag was created.', verbose_name='Created'), ), migrations.AlterField( model_name='flag', name='everyone', field=models.NullBooleanField(help_text='Flip this flag on (Yes) or off (No) for everyone, overriding all other settings. Leave as Unknown to use normally.', verbose_name='Everyone'), ), migrations.AlterField( model_name='flag', name='groups', field=models.ManyToManyField(blank=True, help_text='Activate this flag for these user groups.', to='auth.Group', verbose_name='Groups'), ), migrations.AlterField( model_name='flag', name='languages', field=models.TextField(blank=True, default='', help_text='Activate this flag for users with one of these languages (comma-separated list)', verbose_name='Languages'), ), migrations.AlterField( model_name='flag', name='modified', field=models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this Flag was last modified.', verbose_name='Modified'), ), migrations.AlterField( model_name='flag', name='name', field=models.CharField(help_text='The human/computer readable name.', max_length=100, unique=True, verbose_name='Name'), ), migrations.AlterField( model_name='flag', name='note', field=models.TextField(blank=True, help_text='Note where this Flag is used.', verbose_name='Note'), ), migrations.AlterField( model_name='flag', name='percent', field=models.DecimalField(blank=True, decimal_places=1, help_text='A number between 0.0 and 99.9 to indicate a percentage of users for whom this flag will be active.', max_digits=3, null=True, verbose_name='Percent'), ), migrations.AlterField( model_name='flag', name='rollout', field=models.BooleanField(default=False, help_text='Activate roll-out mode?', verbose_name='Rollout'), ), migrations.AlterField( model_name='flag', name='staff', field=models.BooleanField(default=False, help_text='Flag always active for staff?', verbose_name='Staff'), ), migrations.AlterField( model_name='flag', name='superusers', field=models.BooleanField(default=True, help_text='Flag always active for superusers?', verbose_name='Superusers'), ), migrations.AlterField( model_name='flag', name='testing', field=models.BooleanField(default=False, help_text='Allow this flag to be set for a session for user testing', verbose_name='Testing'), ), migrations.AlterField( model_name='flag', name='users', field=models.ManyToManyField(blank=True, help_text='Activate this flag for these users.', to=settings.AUTH_USER_MODEL, verbose_name='Users'), ), migrations.AlterField( model_name='sample', name='created', field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, help_text='Date when this Sample was created.', verbose_name='Created'), ), migrations.AlterField( model_name='sample', name='modified', field=models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this Sample was last modified.', verbose_name='Modified'), ), migrations.AlterField( model_name='sample', name='name', field=models.CharField(help_text='The human/computer readable name.', max_length=100, unique=True, verbose_name='Name'), ), migrations.AlterField( model_name='sample', name='note', field=models.TextField(blank=True, help_text='Note where this Sample is used.', verbose_name='Note'), ), migrations.AlterField( model_name='sample', name='percent', field=models.DecimalField(decimal_places=1, help_text='A number between 0.0 and 100.0 to indicate a percentage of the time this sample will be active.', max_digits=4, verbose_name='Percent'), ), migrations.AlterField( model_name='switch', name='active', field=models.BooleanField(default=False, help_text='Is this switch active?', verbose_name='Active'), ), migrations.AlterField( model_name='switch', name='created', field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, help_text='Date when this Switch was created.', verbose_name='Created'), ), migrations.AlterField( model_name='switch', name='modified', field=models.DateTimeField(default=django.utils.timezone.now, help_text='Date when this Switch was last modified.', verbose_name='Modified'), ), migrations.AlterField( model_name='switch', name='name', field=models.CharField(help_text='The human/computer readable name.', max_length=100, unique=True, verbose_name='Name'), ), migrations.AlterField( model_name='switch', name='note', field=models.TextField(blank=True, help_text='Note where this Switch is used.', verbose_name='Note'), ), ] django-waffle-0.20.0/waffle/migrations/__init__.py000066400000000000000000000000001362207625100220200ustar00rootroot00000000000000django-waffle-0.20.0/waffle/mixins.py000066400000000000000000000034371362207625100174350ustar00rootroot00000000000000from functools import partial from django.http import Http404 from waffle import switch_is_active, flag_is_active, sample_is_active class BaseWaffleMixin(object): def validate_waffle(self, waffle, func): if waffle.startswith('!'): active = not func(waffle[1:]) else: active = func(waffle) return active def invalid_waffle(self): raise Http404('Inactive waffle') class WaffleFlagMixin(BaseWaffleMixin): """ Checks that as flag is active, or 404. Operates like the FBV decorator waffle_flag """ waffle_flag = None def dispatch(self, request, *args, **kwargs): func = partial(flag_is_active, request) active = self.validate_waffle(self.waffle_flag, func) if not active: return self.invalid_waffle() return super(WaffleFlagMixin, self).dispatch(request, *args, **kwargs) class WaffleSampleMixin(BaseWaffleMixin): """ Checks that as switch is active, or 404. Operates like the FBV decorator waffle_sample. """ waffle_sample = None def dispatch(self, request, *args, **kwargs): active = self.validate_waffle(self.waffle_sample, sample_is_active) if not active: return self.invalid_waffle() return super(WaffleSampleMixin, self).dispatch(request, *args, **kwargs) class WaffleSwitchMixin(BaseWaffleMixin): """ Checks that as switch is active, or 404. Operates like the FBV decorator waffle_switch. """ waffle_switch = None def dispatch(self, request, *args, **kwargs): active = self.validate_waffle(self.waffle_switch, switch_is_active) if not active: return self.invalid_waffle() return super(WaffleSwitchMixin, self).dispatch(request, *args, **kwargs) django-waffle-0.20.0/waffle/models.py000066400000000000000000000367061362207625100174160ustar00rootroot00000000000000from __future__ import unicode_literals import random from decimal import Decimal import logging import six from django.conf import settings from django.contrib.auth.models import Group from django.db import models, router, transaction from django.utils import timezone if six.PY2: from django.utils.translation import ugettext_lazy as _ else: from django.utils.translation import gettext_lazy as _ from six import python_2_unicode_compatible from waffle import managers, get_waffle_flag_model from waffle.utils import get_setting, keyfmt, get_cache logger = logging.getLogger('waffle') CACHE_EMPTY = '-' @python_2_unicode_compatible class BaseModel(models.Model): SINGLE_CACHE_KEY = '' ALL_CACHE_KEY = '' class Meta(object): abstract = True def __str__(self): return self.name def natural_key(self): return (self.name,) @classmethod def _cache_key(cls, name): return keyfmt(get_setting(cls.SINGLE_CACHE_KEY), name) @classmethod def get(cls, name): cache = get_cache() cache_key = cls._cache_key(name) cached = cache.get(cache_key) if cached == CACHE_EMPTY: return cls(name=name) if cached: return cached try: obj = cls.get_from_db(name) except cls.DoesNotExist: cache.add(cache_key, CACHE_EMPTY) return cls(name=name) cache.add(cache_key, obj) return obj @classmethod def get_from_db(cls, name): objects = cls.objects if get_setting('READ_FROM_WRITE_DB'): objects = objects.using(router.db_for_write(cls)) return objects.get(name=name) @classmethod def get_all(cls): cache = get_cache() cache_key = get_setting(cls.ALL_CACHE_KEY) cached = cache.get(cache_key) if cached == CACHE_EMPTY: return [] if cached: return cached objs = cls.get_all_from_db() if not objs: cache.add(cache_key, CACHE_EMPTY) return [] cache.add(cache_key, objs) return objs @classmethod def get_all_from_db(cls): objects = cls.objects if get_setting('READ_FROM_WRITE_DB'): objects = objects.using(router.db_for_write(cls)) return list(objects.all()) def flush(self): cache = get_cache() keys = [ self._cache_key(self.name), get_setting(self.ALL_CACHE_KEY), ] cache.delete_many(keys) def save(self, *args, **kwargs): self.modified = timezone.now() ret = super(BaseModel, self).save(*args, **kwargs) if hasattr(transaction, 'on_commit'): transaction.on_commit(self.flush) else: self.flush() return ret def delete(self, *args, **kwargs): ret = super(BaseModel, self).delete(*args, **kwargs) if hasattr(transaction, 'on_commit'): transaction.on_commit(self.flush) else: self.flush() return ret def set_flag(request, flag_name, active=True, session_only=False): """Set a flag value on a request object.""" if not hasattr(request, 'waffles'): request.waffles = {} request.waffles[flag_name] = [active, session_only] class AbstractBaseFlag(BaseModel): """A feature flag. Flags are active (or not) on a per-request basis. """ name = models.CharField( max_length=100, unique=True, help_text=_('The human/computer readable name.'), verbose_name=_('Name'), ) everyone = models.NullBooleanField( blank=True, help_text=_( 'Flip this flag on (Yes) or off (No) for everyone, overriding all ' 'other settings. Leave as Unknown to use normally.'), verbose_name=_('Everyone'), ) percent = models.DecimalField( max_digits=3, decimal_places=1, null=True, blank=True, help_text=_('A number between 0.0 and 99.9 to indicate a percentage of ' 'users for whom this flag will be active.'), verbose_name=_('Percent'), ) testing = models.BooleanField( default=False, help_text=_('Allow this flag to be set for a session for user testing'), verbose_name=_('Testing'), ) superusers = models.BooleanField( default=True, help_text=_('Flag always active for superusers?'), verbose_name=_('Superusers'), ) staff = models.BooleanField( default=False, help_text=_('Flag always active for staff?'), verbose_name=_('Staff'), ) authenticated = models.BooleanField( default=False, help_text=_('Flag always active for authenticated users?'), verbose_name=_('Authenticated'), ) languages = models.TextField( blank=True, default='', help_text=_('Activate this flag for users with one of these languages (comma-separated list)'), verbose_name=_('Languages'), ) rollout = models.BooleanField( default=False, help_text=_('Activate roll-out mode?'), verbose_name=_('Rollout'), ) note = models.TextField( blank=True, help_text=_('Note where this Flag is used.'), verbose_name=_('Note'), ) created = models.DateTimeField( default=timezone.now, db_index=True, help_text=_('Date when this Flag was created.'), verbose_name=_('Created'), ) modified = models.DateTimeField( default=timezone.now, help_text=_('Date when this Flag was last modified.'), verbose_name=_('Modified'), ) objects = managers.FlagManager() SINGLE_CACHE_KEY = 'FLAG_CACHE_KEY' ALL_CACHE_KEY = 'ALL_FLAGS_CACHE_KEY' class Meta: abstract = True verbose_name = _('Flag') verbose_name_plural = _('Flags') def flush(self): cache = get_cache() keys = self.get_flush_keys() cache.delete_many(keys) def get_flush_keys(self, flush_keys=None): flush_keys = flush_keys or [] flush_keys.extend([ self._cache_key(self.name), get_setting('ALL_FLAGS_CACHE_KEY'), ]) return flush_keys def is_active_for_user(self, user): if self.authenticated and user.is_authenticated: return True if self.staff and getattr(user, 'is_staff', False): return True if self.superusers and getattr(user, 'is_superuser', False): return True return None def _is_active_for_user(self, request): return self.is_active_for_user(request.user) def _is_active_for_language(self, request): if self.languages: languages = [ln.strip() for ln in self.languages.split(',')] if (hasattr(request, 'LANGUAGE_CODE') and request.LANGUAGE_CODE in languages): return True return None def is_active(self, request): if not self.pk: log_level = get_setting('LOG_MISSING_FLAGS') if log_level: logger.log(log_level, 'Flag %s not found', self.name) if get_setting('CREATE_MISSING_FLAGS'): get_waffle_flag_model().objects.get_or_create( name=self.name, defaults={ 'everyone': get_setting('FLAG_DEFAULT') } ) return get_setting('FLAG_DEFAULT') if get_setting('OVERRIDE'): if self.name in request.GET: return request.GET[self.name] == '1' if self.everyone: return True elif self.everyone is False: return False if self.testing: # Testing mode is on. tc = get_setting('TEST_COOKIE') % self.name if tc in request.GET: on = request.GET[tc] == '1' if not hasattr(request, 'waffle_tests'): request.waffle_tests = {} request.waffle_tests[self.name] = on return on if tc in request.COOKIES: return request.COOKIES[tc] == 'True' active_for_language = self._is_active_for_language(request) if active_for_language is not None: return active_for_language active_for_user = self._is_active_for_user(request) if active_for_user is not None: return active_for_user if self.percent and self.percent > 0: if not hasattr(request, 'waffles'): request.waffles = {} elif self.name in request.waffles: return request.waffles[self.name][0] cookie = get_setting('COOKIE') % self.name if cookie in request.COOKIES: flag_active = (request.COOKIES[cookie] == 'True') set_flag(request, self.name, flag_active, self.rollout) return flag_active if Decimal(str(random.uniform(0, 100))) <= self.percent: set_flag(request, self.name, True, self.rollout) return True set_flag(request, self.name, False, self.rollout) return False class AbstractUserFlag(AbstractBaseFlag): groups = models.ManyToManyField( Group, blank=True, help_text=_('Activate this flag for these user groups.'), verbose_name=_('Groups'), ) users = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, help_text=_('Activate this flag for these users.'), verbose_name=_('Users'), ) class Meta(AbstractBaseFlag.Meta): abstract = True verbose_name = _('Flag') verbose_name_plural = _('Flags') def get_flush_keys(self, flush_keys=None): flush_keys = super(AbstractUserFlag, self).get_flush_keys(flush_keys) flush_keys.extend([ keyfmt(get_setting('FLAG_USERS_CACHE_KEY'), self.name), keyfmt(get_setting('FLAG_GROUPS_CACHE_KEY'), self.name), ]) return flush_keys def _get_user_ids(self): cache = get_cache() cache_key = keyfmt(get_setting('FLAG_USERS_CACHE_KEY'), self.name) cached = cache.get(cache_key) if cached == CACHE_EMPTY: return set() if cached: return cached user_ids = set(self.users.all().values_list('pk', flat=True)) if not user_ids: cache.add(cache_key, CACHE_EMPTY) return set() cache.add(cache_key, user_ids) return user_ids def _get_group_ids(self): cache = get_cache() cache_key = keyfmt(get_setting('FLAG_GROUPS_CACHE_KEY'), self.name) cached = cache.get(cache_key) if cached == CACHE_EMPTY: return set() if cached: return cached group_ids = set(self.groups.all().values_list('pk', flat=True)) if not group_ids: cache.add(cache_key, CACHE_EMPTY) return set() cache.add(cache_key, group_ids) return group_ids def is_active_for_user(self, user): is_active = super(AbstractUserFlag, self).is_active_for_user(user) if is_active: return is_active user_ids = self._get_user_ids() if hasattr(user, 'pk') and user.pk in user_ids: return True if hasattr(user, 'groups'): group_ids = self._get_group_ids() if group_ids: user_groups = set(user.groups.all().values_list('pk', flat=True)) if group_ids.intersection(user_groups): return True return None class Flag(AbstractUserFlag): """A feature flag. Flags are active (or not) on a per-request basis. """ class Meta(AbstractUserFlag.Meta): swappable = 'WAFFLE_FLAG_MODEL' verbose_name = _('Flag') verbose_name_plural = _('Flags') class Switch(BaseModel): """A feature switch. Switches are active, or inactive, globally. """ name = models.CharField( max_length=100, unique=True, help_text=_('The human/computer readable name.'), verbose_name=_('Name'), ) active = models.BooleanField( default=False, help_text=_('Is this switch active?'), verbose_name=_('Active'), ) note = models.TextField( blank=True, help_text=_('Note where this Switch is used.'), verbose_name=_('Note'), ) created = models.DateTimeField( default=timezone.now, db_index=True, help_text=_('Date when this Switch was created.'), verbose_name=_('Created'), ) modified = models.DateTimeField( default=timezone.now, help_text=_('Date when this Switch was last modified.'), verbose_name=_('Modified'), ) objects = managers.SwitchManager() SINGLE_CACHE_KEY = 'SWITCH_CACHE_KEY' ALL_CACHE_KEY = 'ALL_SWITCHES_CACHE_KEY' class Meta: verbose_name = _('Switch') verbose_name_plural = _('Switches') def is_active(self): if not self.pk: log_level = get_setting('LOG_MISSING_SWITCHES') if log_level: logger.log(log_level, 'Switch %s not found', self.name) if get_setting('CREATE_MISSING_SWITCHES'): Switch.objects.get_or_create( name=self.name, defaults={ 'active': get_setting('SWITCH_DEFAULT') } ) return get_setting('SWITCH_DEFAULT') return self.active class Sample(BaseModel): """A sample of users. A sample is true some percentage of the time, but is not connected to users or requests. """ name = models.CharField( max_length=100, unique=True, help_text=_('The human/computer readable name.'), verbose_name=_('Name'), ) percent = models.DecimalField( max_digits=4, decimal_places=1, help_text=_('A number between 0.0 and 100.0 to indicate a percentage of the time ' 'this sample will be active.'), verbose_name=_('Percent'), ) note = models.TextField( blank=True, help_text=_('Note where this Sample is used.'), verbose_name=_('Note'), ) created = models.DateTimeField( default=timezone.now, db_index=True, help_text=_('Date when this Sample was created.'), verbose_name=_('Created'), ) modified = models.DateTimeField( default=timezone.now, help_text=_('Date when this Sample was last modified.'), verbose_name=_('Modified'), ) objects = managers.SampleManager() SINGLE_CACHE_KEY = 'SAMPLE_CACHE_KEY' ALL_CACHE_KEY = 'ALL_SAMPLES_CACHE_KEY' class Meta: verbose_name = _('Sample') verbose_name_plural = _('Samples') def is_active(self): if not self.pk: log_level = get_setting('LOG_MISSING_SAMPLES') if log_level: logger.log(log_level, 'Sample %s not found', self.name) if get_setting('CREATE_MISSING_SAMPLES'): default_percent = 100 if get_setting('SAMPLE_DEFAULT') else 0 Sample.objects.get_or_create( name=self.name, defaults={ 'percent': default_percent } ) return get_setting('SAMPLE_DEFAULT') return Decimal(str(random.uniform(0, 100))) <= self.percent django-waffle-0.20.0/waffle/signals.py000066400000000000000000000006251362207625100175620ustar00rootroot00000000000000from django.db.models.signals import m2m_changed from django.dispatch import receiver from waffle import get_waffle_flag_model @receiver(m2m_changed, sender=get_waffle_flag_model().users.through) @receiver(m2m_changed, sender=get_waffle_flag_model().groups.through) def flag_membership_changed(sender, instance, action, **kwargs): if action in ('post_add', 'post_remove'): instance.flush() django-waffle-0.20.0/waffle/templates/000077500000000000000000000000001362207625100175435ustar00rootroot00000000000000django-waffle-0.20.0/waffle/templates/waffle/000077500000000000000000000000001362207625100210075ustar00rootroot00000000000000django-waffle-0.20.0/waffle/templates/waffle/waffle.js000066400000000000000000000023421362207625100226120ustar00rootroot00000000000000(function(){ var FLAGS = { {% for flag, value in flags %}'{{ flag }}': {% if value %}true{% else %}false{% endif %}{% if not forloop.last %},{% endif %}{% endfor %} }, SWITCHES = { {% for switch, value in switches %}'{{ switch }}': {% if value %}true{% else %}false{% endif %}{% if not forloop.last %},{% endif %}{% endfor %} }, SAMPLES = { {% for sample, value in samples %}'{{ sample }}': {% if value %}true{% else %}false{% endif %}{% if not forloop.last %},{% endif %}{% endfor %} }; window.waffle = { "flag_is_active": function waffle_flag(flag_name) { {% if flag_default %} if(FLAGS[flag_name] === undefined) return true; {% endif %} return !!FLAGS[flag_name]; }, "switch_is_active": function waffle_switch(switch_name) { {% if switch_default %} if(SWITCHES[switch_name] === undefined) return true; {% endif %} return !!SWITCHES[switch_name]; }, "sample_is_active": function waffle_sample(sample_name) { {% if sample_default %} if(SAMPLES[sample_name] === undefined) return true; {% endif %} return !!SAMPLES[sample_name]; }, "FLAGS": FLAGS, "SWITCHES": SWITCHES, "SAMPLES": SAMPLES }; })(); django-waffle-0.20.0/waffle/templatetags/000077500000000000000000000000001362207625100202375ustar00rootroot00000000000000django-waffle-0.20.0/waffle/templatetags/__init__.py000066400000000000000000000000001362207625100223360ustar00rootroot00000000000000django-waffle-0.20.0/waffle/templatetags/waffle_tags.py000066400000000000000000000051221362207625100230730ustar00rootroot00000000000000from __future__ import unicode_literals from django import template from django.template.base import VariableDoesNotExist from waffle import flag_is_active, sample_is_active, switch_is_active from waffle.views import _generate_waffle_js register = template.Library() class WaffleNode(template.Node): child_nodelists = ('nodelist_true', 'nodelist_false') def __init__(self, nodelist_true, nodelist_false, condition, name, compiled_name): self.nodelist_true = nodelist_true self.nodelist_false = nodelist_false self.condition = condition self.name = name self.compiled_name = compiled_name def __repr__(self): return '' % self.name def __iter__(self): for node in self.nodelist_true: yield node for node in self.nodelist_false: yield node def render(self, context): try: name = self.compiled_name.resolve(context) except VariableDoesNotExist: name = self.name if not name: name = self.name if self.condition(context.get('request', None), name): return self.nodelist_true.render(context) return self.nodelist_false.render(context) @classmethod def handle_token(cls, parser, token, kind, condition): bits = token.split_contents() if len(bits) < 2: raise template.TemplateSyntaxError("%r tag requires an argument" % bits[0]) name = bits[1] compiled_name = parser.compile_filter(name) nodelist_true = parser.parse(('else', 'end%s' % kind)) token = parser.next_token() if token.contents == 'else': nodelist_false = parser.parse(('end%s' % kind,)) parser.delete_first_token() else: nodelist_false = template.NodeList() return cls(nodelist_true, nodelist_false, condition, name, compiled_name) @register.tag def flag(parser, token): return WaffleNode.handle_token(parser, token, 'flag', flag_is_active) @register.tag def switch(parser, token): return WaffleNode.handle_token(parser, token, 'switch', lambda request, name: switch_is_active(name)) @register.tag def sample(parser, token): return WaffleNode.handle_token(parser, token, 'sample', lambda request, name: sample_is_active(name)) class InlineWaffleJSNode(template.Node): def render(self, context): return _generate_waffle_js(context['request']) @register.tag def wafflejs(parser, token): return InlineWaffleJSNode() django-waffle-0.20.0/waffle/tests/000077500000000000000000000000001362207625100167075ustar00rootroot00000000000000django-waffle-0.20.0/waffle/tests/__init__.py000066400000000000000000000000001362207625100210060ustar00rootroot00000000000000django-waffle-0.20.0/waffle/tests/base.py000066400000000000000000000012721362207625100201750ustar00rootroot00000000000000from __future__ import unicode_literals from django import test from django.core import cache class TestCase(test.TransactionTestCase): def _pre_setup(self): cache.cache.clear() super(TestCase, self)._pre_setup() class ReplicationRouter(object): """Router for simulating an environment with DB replication This router directs all DB reads to a completely different database than writes. This can be useful for simulating an environment where DB replication is delayed to identify potential race conditions. """ def db_for_read(self, model, **hints): return 'readonly' def db_for_write(self, model, **hints): return 'default' django-waffle-0.20.0/waffle/tests/test_admin.py000077500000000000000000000134001362207625100214110ustar00rootroot00000000000000from __future__ import unicode_literals try: import mock except ImportError: import unittest.mock as mock import unittest import django from django.contrib.admin.models import LogEntry, CHANGE, DELETION from django.contrib.admin.sites import AdminSite from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission from waffle import get_waffle_flag_model from waffle.admin import (FlagAdmin, SwitchAdmin, InformativeManyToManyRawIdWidget, enable_for_all, disable_for_all, delete_individually, enable_switches, disable_switches) from waffle.models import Switch from waffle.tests.base import TestCase django_version = tuple(int(d) for d in django.get_version().split(".")) class FakeSuperUser: def has_perm(self, perm): return True class FakeRequest: def __init__(self): self.GET = {} self.user = get_user_model().objects.create(username="test1") Flag = get_waffle_flag_model() skip_if_admin_permissions_not_available = \ unittest.skipIf(django_version < (2, 1, 0), "Feature not available") class FlagAdminTests(TestCase): def setUp(self): self.site = AdminSite() self.flag_admin = FlagAdmin(Flag, self.site) def test_informative_widget(self): request = mock.Mock() request.has_perm = lambda self, perm: True form = self.flag_admin.get_form(request)() user_widget = form.fields["users"].widget self.assertIsInstance(user_widget, InformativeManyToManyRawIdWidget) user1 = get_user_model().objects.create(username="test1") user2 = get_user_model().objects.create(username="test2") self.assertIn("(test1, test2)", user_widget.render("users", [user1.pk, user2.pk])) def test_enable_for_all(self): f1 = Flag.objects.create(name="flag1", everyone=False) request = FakeRequest() enable_for_all(None, request, Flag.objects.all()) f1.refresh_from_db() self.assertTrue(f1.everyone) log_entry = LogEntry.objects.get(user=request.user) self.assertEqual(log_entry.action_flag, CHANGE) self.assertEqual(log_entry.object_repr, "flag1 on") def test_disable_for_all(self): f1 = Flag.objects.create(name="flag1", everyone=True) request = FakeRequest() disable_for_all(None, request, Flag.objects.all()) f1.refresh_from_db() self.assertFalse(f1.everyone) log_entry = LogEntry.objects.get(user=request.user) self.assertEqual(log_entry.action_flag, CHANGE) self.assertEqual(log_entry.object_repr, "flag1 off") def test_delete_individually(self): Flag.objects.create(name="flag1", everyone=True) request = FakeRequest() delete_individually(None, request, Flag.objects.all()) self.assertIsNone(Flag.objects.first()) log_entry = LogEntry.objects.get(user=request.user) self.assertEqual(log_entry.action_flag, DELETION) @skip_if_admin_permissions_not_available def test_flag_no_actions_without_permissions(self): request = FakeRequest() actions = self.flag_admin.get_actions(request) self.assertEqual(actions.keys(), set()) @skip_if_admin_permissions_not_available def test_flag_action_change(self): request = FakeRequest() request.user.user_permissions.add(Permission.objects.get(codename="change_flag")) actions = self.flag_admin.get_actions(request) self.assertEqual(actions.keys(), {"enable_for_all", "disable_for_all"}) @skip_if_admin_permissions_not_available def test_flag_action_delete(self): request = FakeRequest() request.user.user_permissions.add(Permission.objects.get(codename="delete_flag")) actions = self.flag_admin.get_actions(request) self.assertEqual(actions.keys(), {"delete_individually"}) class SwitchAdminTests(TestCase): def setUp(self): self.site = AdminSite() self.switch_admin = SwitchAdmin(Switch, self.site) def test_enable_switches(self): s1 = Switch.objects.create(name="switch1", active=False) request = FakeRequest() enable_switches(None, request, Switch.objects.all()) s1.refresh_from_db() self.assertTrue(s1.active) log_entry = LogEntry.objects.get(user=request.user) self.assertEqual(log_entry.action_flag, CHANGE) self.assertEqual(log_entry.object_repr, "switch1 on") def test_disable_switches(self): s1 = Switch.objects.create(name="switch1", active=True) request = FakeRequest() disable_switches(None, request, Switch.objects.all()) s1.refresh_from_db() self.assertFalse(s1.active) log_entry = LogEntry.objects.get(user=request.user) self.assertEqual(log_entry.action_flag, CHANGE) self.assertEqual(log_entry.object_repr, "switch1 off") @skip_if_admin_permissions_not_available def test_switch_no_actions_without_permissions(self): request = FakeRequest() actions = self.switch_admin.get_actions(request) self.assertEqual(actions.keys(), set()) @skip_if_admin_permissions_not_available def test_switch_action_change(self): request = FakeRequest() request.user.user_permissions.add(Permission.objects.get(codename="change_switch")) actions = self.switch_admin.get_actions(request) self.assertEqual(actions.keys(), {"enable_switches", "disable_switches"}) @skip_if_admin_permissions_not_available def test_switch_action_delete(self): request = FakeRequest() request.user.user_permissions.add(Permission.objects.get(codename="delete_switch")) actions = self.switch_admin.get_actions(request) self.assertEqual(actions.keys(), {"delete_individually"}) django-waffle-0.20.0/waffle/tests/test_decorators.py000066400000000000000000000121221362207625100224630ustar00rootroot00000000000000from __future__ import unicode_literals from waffle import get_waffle_flag_model from waffle.models import Switch from waffle.tests.base import TestCase class DecoratorTests(TestCase): def test_flag_must_be_active(self): resp = self.client.get('/flag-on') self.assertEqual(404, resp.status_code) get_waffle_flag_model().objects.create(name='foo', everyone=True) resp = self.client.get('/flag-on') self.assertEqual(200, resp.status_code) def test_flag_must_be_inactive(self): resp = self.client.get('/flag-off') self.assertEqual(200, resp.status_code) get_waffle_flag_model().objects.create(name='foo', everyone=True) resp = self.client.get('/flag-off') self.assertEqual(404, resp.status_code) def test_switch_must_be_active(self): resp = self.client.get('/switch-on') self.assertEqual(404, resp.status_code) Switch.objects.create(name='foo', active=True) resp = self.client.get('/switch-on') self.assertEqual(200, resp.status_code) def test_switch_must_be_inactive(self): resp = self.client.get('/switch-off') self.assertEqual(200, resp.status_code) Switch.objects.create(name='foo', active=True) resp = self.client.get('/switch-off') self.assertEqual(404, resp.status_code) def test_switch_must_be_inactive_and_redirect_to_view(self): resp = self.client.get('/switched_view_with_valid_redirect') self.assertEqual(302, resp.status_code) Switch.objects.create(name='foo', active=True) resp = self.client.get('/switched_view_with_valid_redirect') self.assertEqual(200, resp.status_code) def test_switch_must_be_inactive_and_redirect_to_named_view(self): resp = self.client.get('/switched_view_with_valid_url_name') self.assertEqual(302, resp.status_code) Switch.objects.create(name='foo', active=True) resp = self.client.get('/switched_view_with_valid_url_name') self.assertEqual(200, resp.status_code) def test_switch_must_be_inactive_and_redirect_to_view_with_args(self): resp = self.client.get('/switched_view_with_args_with_valid_redirect/1/') self.assertRedirects(resp, '/foo_view_with_args/1/') Switch.objects.create(name='foo', active=True) resp = self.client.get('/switched_view_with_args_with_valid_redirect/1/') self.assertEqual(200, resp.status_code) def test_switch_must_be_inactive_and_redirect_to_named_view_with_args(self): resp = self.client.get('/switched_view_with_args_with_valid_url_name/1/') self.assertRedirects(resp, '/foo_view_with_args/1/') Switch.objects.create(name='foo', active=True) resp = self.client.get('/switched_view_with_args_with_valid_url_name/1/') self.assertEqual(200, resp.status_code) def test_switch_must_be_inactive_and_not_redirect(self): resp = self.client.get('/switched_view_with_invalid_redirect') self.assertEqual(404, resp.status_code) Switch.objects.create(name='foo', active=True) resp = self.client.get('/switched_view_with_invalid_redirect') self.assertEqual(200, resp.status_code) def test_flag_must_be_inactive_and_redirect_to_view(self): resp = self.client.get('/flagged_view_with_valid_redirect') self.assertEqual(302, resp.status_code) get_waffle_flag_model().objects.create(name='foo', everyone=True) resp = self.client.get('/flagged_view_with_valid_redirect') self.assertEqual(200, resp.status_code) def test_flag_must_be_inactive_and_redirect_to_named_view(self): resp = self.client.get('/flagged_view_with_valid_url_name') self.assertEqual(302, resp.status_code) get_waffle_flag_model().objects.create(name='foo', everyone=True) resp = self.client.get('/flagged_view_with_valid_url_name') self.assertEqual(200, resp.status_code) def test_flag_must_be_inactive_and_redirect_to_view_with_args(self): resp = self.client.get('/flagged_view_with_args_with_valid_redirect/1/') self.assertRedirects(resp, '/foo_view_with_args/1/') get_waffle_flag_model().objects.create(name='foo', everyone=True) resp = self.client.get('/flagged_view_with_args_with_valid_redirect/1/') self.assertEqual(200, resp.status_code) def test_flag_must_be_inactive_and_redirect_to_named_view_with_args(self): resp = self.client.get('/flagged_view_with_args_with_valid_url_name/1/') self.assertRedirects(resp, '/foo_view_with_args/1/') get_waffle_flag_model().objects.create(name='foo', everyone=True) resp = self.client.get('/flagged_view_with_args_with_valid_url_name/1/') self.assertEqual(200, resp.status_code) def test_flag_must_be_inactive_and_not_redirect(self): resp = self.client.get('/flagged_view_with_invalid_redirect') self.assertEqual(404, resp.status_code) get_waffle_flag_model().objects.create(name='foo', everyone=True) resp = self.client.get('/flagged_view_with_invalid_redirect') self.assertEqual(200, resp.status_code) django-waffle-0.20.0/waffle/tests/test_management.py000066400000000000000000000306211362207625100224360ustar00rootroot00000000000000from __future__ import unicode_literals import six from django.core.management import call_command, CommandError from django.contrib.auth.models import Group, User from waffle import get_waffle_flag_model from waffle.models import Sample, Switch from waffle.tests.base import TestCase class WaffleFlagManagementCommandTests(TestCase): def test_create(self): """ The command should create a new flag. """ name = 'test' percent = 20 Group.objects.create(name='waffle_group') call_command('waffle_flag', name, percent=percent, superusers=True, staff=True, authenticated=True, rollout=True, create=True, group=['waffle_group']) flag = get_waffle_flag_model().objects.get(name=name) self.assertEqual(flag.percent, percent) self.assertIsNone(flag.everyone) self.assertTrue(flag.superusers) self.assertTrue(flag.staff) self.assertTrue(flag.authenticated) self.assertTrue(flag.rollout) self.assertEqual(list(flag.groups.values_list('name', flat=True)), ['waffle_group']) def test_not_create(self): """ The command shouldn't create a new flag if the create flag is not set. """ name = 'test' with self.assertRaisesRegexp(CommandError, 'This flag does not exist.'): call_command('waffle_flag', name, everyone=True, percent=20, superusers=True, staff=True, authenticated=True, rollout=True) self.assertFalse(get_waffle_flag_model().objects.filter(name=name).exists()) def test_update(self): """ The command should update an existing flag. """ name = 'test' flag = get_waffle_flag_model().objects.create(name=name) self.assertIsNone(flag.percent) self.assertIsNone(flag.everyone) self.assertTrue(flag.superusers) self.assertFalse(flag.staff) self.assertFalse(flag.authenticated) self.assertFalse(flag.rollout) percent = 30 call_command('waffle_flag', name, percent=percent, superusers=False, staff=True, authenticated=True, rollout=True) flag.refresh_from_db() self.assertEqual(flag.percent, percent) self.assertIsNone(flag.everyone) self.assertFalse(flag.superusers) self.assertTrue(flag.staff) self.assertTrue(flag.authenticated) self.assertTrue(flag.rollout) def test_update_activate_everyone(self): """ The command should update everyone field to True """ name = 'test' flag = get_waffle_flag_model().objects.create(name=name) self.assertIsNone(flag.percent) self.assertIsNone(flag.everyone) self.assertTrue(flag.superusers) self.assertFalse(flag.staff) self.assertFalse(flag.authenticated) self.assertFalse(flag.rollout) percent = 30 call_command('waffle_flag', name, everyone=True, percent=percent, superusers=False, staff=True, authenticated=True, rollout=True) flag.refresh_from_db() self.assertEqual(flag.percent, percent) self.assertTrue(flag.everyone) self.assertFalse(flag.superusers) self.assertTrue(flag.staff) self.assertTrue(flag.authenticated) self.assertTrue(flag.rollout) def test_update_deactivate_everyone(self): """ The command should update everyone field to False""" name = 'test' flag = get_waffle_flag_model().objects.create(name=name) self.assertIsNone(flag.percent) self.assertIsNone(flag.everyone) self.assertTrue(flag.superusers) self.assertFalse(flag.staff) self.assertFalse(flag.authenticated) self.assertFalse(flag.rollout) percent = 30 call_command('waffle_flag', name, everyone=False, percent=percent, superusers=False, staff=True, authenticated=True, rollout=True) flag.refresh_from_db() self.assertEqual(flag.percent, percent) self.assertFalse(flag.everyone) self.assertFalse(flag.superusers) self.assertTrue(flag.staff) self.assertTrue(flag.authenticated) self.assertTrue(flag.rollout) def test_list(self): """ The command should list all flags.""" stdout = six.StringIO() get_waffle_flag_model().objects.create(name='test') call_command('waffle_flag', list_flags=True, stdout=stdout) expected = 'Flags:\nNAME: test\nSUPERUSERS: True\nEVERYONE: None\n' \ 'AUTHENTICATED: False\nPERCENT: None\nTESTING: False\n' \ 'ROLLOUT: False\nSTAFF: False\nGROUPS: []\nUSERS: []' actual = stdout.getvalue().strip() self.assertEqual(actual, expected) def test_group_append(self): """ The command should append a group to a flag.""" original_group = Group.objects.create(name='waffle_group') Group.objects.create(name='append_group') flag = get_waffle_flag_model().objects.create(name='test') flag.groups.add(original_group) flag.refresh_from_db() self.assertEqual(list(flag.groups.values_list('name', flat=True)), ['waffle_group']) call_command('waffle_flag', 'test', group=['append_group'], append=True) flag.refresh_from_db() self.assertEqual(list(flag.groups.values_list('name', flat=True)), ['waffle_group', 'append_group']) self.assertIsNone(flag.everyone) def test_user(self): """ The command should replace a user to a flag.""" original_user = User.objects.create_user('waffle_test') User.objects.create_user('add_user') flag = get_waffle_flag_model().objects.create(name='test') flag.users.add(original_user) flag.refresh_from_db() self.assertEqual(list(flag.users.values_list('username', flat=True)), ['waffle_test']) call_command('waffle_flag', 'test', user=['add_user']) flag.refresh_from_db() self.assertEqual(list(flag.users.values_list('username', flat=True)), ['add_user']) self.assertIsNone(flag.everyone) def test_user_append(self): """ The command should append a user to a flag.""" original_user = User.objects.create_user('waffle_test') User.objects.create_user('append_user') User.objects.create_user('append_user_email', email='test@example.com') flag = get_waffle_flag_model().objects.create(name='test') flag.users.add(original_user) flag.refresh_from_db() self.assertEqual(list(flag.users.values_list('username', flat=True)), ['waffle_test']) call_command('waffle_flag', 'test', user=['append_user'], append=True) flag.refresh_from_db() self.assertEqual(list(flag.users.values_list('username', flat=True)), ['waffle_test', 'append_user']) self.assertIsNone(flag.everyone) call_command('waffle_flag', 'test', user=['test@example.com'], append=True) flag.refresh_from_db() self.assertEqual(list(flag.users.values_list('username', flat=True)), ['waffle_test', 'append_user', 'append_user_email']) self.assertIsNone(flag.everyone) class WaffleSampleManagementCommandTests(TestCase): def test_create(self): """ The command should create a new sample. """ name = 'test' percent = 20 call_command('waffle_sample', name, str(percent), create=True) sample = Sample.objects.get(name=name) self.assertEqual(sample.percent, percent) def test_not_create(self): """ The command shouldn't create a new sample if the create flag is not set. """ name = 'test' with self.assertRaisesRegexp(CommandError, 'This sample does not exist'): call_command('waffle_sample', name, '20') self.assertFalse(Sample.objects.filter(name=name).exists()) def test_update(self): """ The command should update an existing sample. """ name = 'test' sample = Sample.objects.create(name=name, percent=0) self.assertEqual(sample.percent, 0) percent = 50 call_command('waffle_sample', name, str(percent)) sample.refresh_from_db() self.assertEqual(sample.percent, percent) def test_list(self): """ The command should list all samples.""" stdout = six.StringIO() Sample.objects.create(name='test', percent=34) call_command('waffle_sample', list_samples=True, stdout=stdout) expected = 'Samples:\ntest: 34.0%' actual = stdout.getvalue().strip() self.assertEqual(actual, expected) class WaffleSwitchManagementCommandTests(TestCase): def test_create(self): """ The command should create a new switch. """ name = 'test' call_command('waffle_switch', name, 'on', create=True) switch = Switch.objects.get(name=name, active=True) switch.delete() call_command('waffle_switch', name, 'off', create=True) Switch.objects.get(name=name, active=False) def test_not_create(self): """ The command shouldn't create a new switch if the create flag is not set. """ name = 'test' with self.assertRaisesRegexp(CommandError, 'This switch does not exist.'): call_command('waffle_switch', name, 'on') self.assertFalse(Switch.objects.filter(name=name).exists()) def test_update(self): """ The command should update an existing switch. """ name = 'test' switch = Switch.objects.create(name=name, active=True) call_command('waffle_switch', name, 'off') switch.refresh_from_db() self.assertFalse(switch.active) call_command('waffle_switch', name, 'on') switch.refresh_from_db() self.assertTrue(switch.active) def test_list(self): """ The command should list all switches.""" stdout = six.StringIO() Switch.objects.create(name='switch1', active=True) Switch.objects.create(name='switch2', active=False) call_command('waffle_switch', list_switches=True, stdout=stdout) expected = 'Switches:\nswitch1: on\nswitch2: off' actual = stdout.getvalue().strip() self.assertEqual(actual, expected) class WaffleDeleteManagementCommandTests(TestCase): def test_delete_flag(self): """ The command should delete a flag. """ name = 'test_flag' get_waffle_flag_model().objects.create(name=name) call_command('waffle_delete', flag_names=[name]) self.assertEqual(get_waffle_flag_model().objects.count(), 0) def test_delete_swtich(self): """ The command should delete a switch. """ name = 'test_switch' Switch.objects.create(name=name) call_command('waffle_delete', switch_names=[name]) self.assertEqual(Switch.objects.count(), 0) def test_delete_sample(self): """ The command should delete a sample. """ name = 'test_sample' Sample.objects.create(name=name, percent=0) call_command('waffle_delete', sample_names=[name]) self.assertEqual(Sample.objects.count(), 0) def test_delete_mix_of_types(self): """ The command should delete different types of records. """ name = 'test' get_waffle_flag_model().objects.create(name=name) Switch.objects.create(name=name) Sample.objects.create(name=name, percent=0) call_command('waffle_delete', switch_names=[name], flag_names=[name], sample_names=[name]) self.assertEqual(get_waffle_flag_model().objects.count(), 0) self.assertEqual(Switch.objects.count(), 0) self.assertEqual(Sample.objects.count(), 0) def test_delete_some_but_not_all_records(self): """ The command should delete specified records, but leave records not specified alone. """ flag_1 = 'test_flag_1' flag_2 = 'test_flag_2' get_waffle_flag_model().objects.create(name=flag_1) get_waffle_flag_model().objects.create(name=flag_2) call_command('waffle_delete', flag_names=[flag_1]) self.assertTrue(get_waffle_flag_model().objects.filter(name=flag_2).exists()) django-waffle-0.20.0/waffle/tests/test_middleware.py000066400000000000000000000031231362207625100224340ustar00rootroot00000000000000from __future__ import unicode_literals from django.http import HttpResponse from django.test import RequestFactory from waffle.middleware import WaffleMiddleware get = RequestFactory().get('/foo') def test_set_cookies(): get.waffles = {'foo': [True, False], 'bar': [False, False]} resp = HttpResponse() assert 'dwf_foo' not in resp.cookies assert 'dwf_bar' not in resp.cookies resp = WaffleMiddleware().process_response(get, resp) assert 'dwf_foo' in resp.cookies assert 'dwf_bar' in resp.cookies assert 'True' == resp.cookies['dwf_foo'].value assert 'False' == resp.cookies['dwf_bar'].value def test_rollout_cookies(): get.waffles = {'foo': [True, True], 'bar': [False, True], 'baz': [True, False], 'qux': [False, False]} resp = HttpResponse() resp = WaffleMiddleware().process_response(get, resp) for k in get.waffles: cookie = 'dwf_%s' % k assert cookie in resp.cookies assert str(get.waffles[k][0]) == resp.cookies[cookie].value if get.waffles[k][1]: assert bool(resp.cookies[cookie]['max-age']) == get.waffles[k][0] else: assert resp.cookies[cookie]['max-age'] def test_testing_cookies(): get.waffles = {} get.waffle_tests = {'foo': True, 'bar': False} resp = HttpResponse() resp = WaffleMiddleware().process_response(get, resp) for k in get.waffle_tests: cookie = 'dwft_%s' % k assert str(get.waffle_tests[k]) == resp.cookies[cookie].value assert not resp.cookies[cookie]['max-age'] django-waffle-0.20.0/waffle/tests/test_mixin.py000066400000000000000000000070061362207625100214470ustar00rootroot00000000000000from django.contrib.auth.models import AnonymousUser from django.http import Http404 from django.test import RequestFactory from test_app import views from waffle.middleware import WaffleMiddleware from waffle.models import Flag, Sample, Switch from waffle.tests.base import TestCase def get(**kw): request = RequestFactory().get('/foo', data=kw) request.user = AnonymousUser() return request def process_request(request, view): response = view.as_view()(request) return WaffleMiddleware().process_response(request, response) class WaffleFlagMixinTest(TestCase): def setUp(self): self.request = get() def test_flag_must_be_active(self): view = views.FlagView self.assertRaises(Http404, process_request, self.request, view) Flag.objects.create(name='foo', everyone=True) response = process_request(self.request, view) self.assertEqual(b'foo', response.content) def test_flag_must_be_inactive(self): view = views.FlagOffView response = process_request(self.request, view) self.assertEqual(b'foo', response.content) Flag.objects.create(name='foo', everyone=True) self.assertRaises(Http404, process_request, self.request, view) def test_override_with_cookie(self): Flag.objects.create(name='foo', percent='0.1') self.request.COOKIES['dwf_foo'] = 'True' response = process_request(self.request, views.FlagView) self.assertEqual(b'foo', response.content) self.assertIn('dwf_foo', response.cookies) self.assertEqual('True', response.cookies['dwf_foo'].value) class WaffleSampleMixinTest(TestCase): def setUp(self): self.request = get() def test_sample_must_be_active(self): view = views.SampleView self.assertRaises(Http404, process_request, self.request, view) Sample.objects.create(name='foo', percent='100.0') response = process_request(self.request, view) self.assertEqual(b'foo', response.content) def test_sample_must_be_inactive(self): view = views.SampleOffView response = process_request(self.request, view) self.assertEqual(b'foo', response.content) Sample.objects.create(name='foo', percent='100.0') self.assertRaises(Http404, process_request, self.request, view) def test_override_with_cookie(self): Sample.objects.create(name='foo', percent='0.0') self.request.COOKIES['dwf_foo'] = 'True' self.assertRaises(Http404, process_request, self.request, views.SwitchView) class WaffleSwitchMixinTest(TestCase): def setUp(self): self.request = get() def test_switch_must_be_active(self): view = views.SwitchView self.assertRaises(Http404, process_request, self.request, view) Switch.objects.create(name='foo', active=True) response = process_request(self.request, view) self.assertEqual(b'foo', response.content) def test_switch_must_be_inactive(self): view = views.SwitchOffView response = process_request(self.request, view) self.assertEqual(b'foo', response.content) Switch.objects.create(name='foo', active=True) self.assertRaises(Http404, process_request, self.request, view) def test_no_override_with_cookie(self): Switch.objects.create(name='foo', active=False) self.request.COOKIES['dwf_foo'] = 'True' self.assertRaises(Http404, process_request, self.request, views.SwitchView) django-waffle-0.20.0/waffle/tests/test_models.py000066400000000000000000000015551362207625100216110ustar00rootroot00000000000000from __future__ import unicode_literals from django.test import TestCase from waffle import get_waffle_flag_model from waffle.models import Switch, Sample class ModelsTests(TestCase): def test_natural_keys(self): flag = get_waffle_flag_model().objects.create(name='test-flag') switch = Switch.objects.create(name='test-switch') sample = Sample.objects.create(name='test-sample', percent=0) self.assertEqual(flag.natural_key(), ('test-flag',)) self.assertEqual(switch.natural_key(), ('test-switch',)) self.assertEqual(sample.natural_key(), ('test-sample',)) self.assertEqual(get_waffle_flag_model().objects.get_by_natural_key('test-flag'), flag) self.assertEqual(Switch.objects.get_by_natural_key('test-switch'), switch) self.assertEqual(Sample.objects.get_by_natural_key('test-sample'), sample) django-waffle-0.20.0/waffle/tests/test_templates.py000066400000000000000000000044671362207625100223310ustar00rootroot00000000000000from __future__ import unicode_literals from unittest import skipUnless from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.template import Template from django.template.base import VariableNode from django.test import RequestFactory from test_app import views from waffle.middleware import WaffleMiddleware from waffle.tests.base import TestCase def get(): request = RequestFactory().get('/foo') request.user = AnonymousUser() return request def process_request(request, view): response = view(request) return WaffleMiddleware().process_response(request, response) class WaffleTemplateTests(TestCase): def test_django_tags(self): request = get() response = process_request(request, views.flag_in_django) self.assertContains(response, 'flag off') self.assertContains(response, 'switch off') self.assertContains(response, 'sample') self.assertContains(response, 'flag_var off') self.assertContains(response, 'switch_var off') self.assertContains(response, 'sample_var') self.assertContains(response, 'window.waffle =') def test_get_nodes_by_type(self): """WaffleNode.get_nodes_by_type() should find all child nodes.""" test_template = Template( '{% load waffle_tags %}' '{% switch "x" %}' '{{ a }}' '{% else %}' '{{ b }}' '{% endswitch %}' ) children = test_template.nodelist.get_nodes_by_type(VariableNode) self.assertEqual(len(children), 2) def test_no_request_context(self): """Switches and Samples shouldn't require a request context.""" request = get() content = process_request(request, views.no_request_context) assert 'switch off' in content assert 'sample' in content @skipUnless( settings.JINJA_INSTALLED, "django-jinja is currently unmaintained and uncompatible with django >= 3.0" ) def test_jinja_tags(self): request = get() response = process_request(request, views.flag_in_jinja) self.assertContains(response, 'flag off') self.assertContains(response, 'switch off') self.assertContains(response, 'sample') self.assertContains(response, 'window.waffle =') django-waffle-0.20.0/waffle/tests/test_testutils.py000066400000000000000000000273161362207625100223710ustar00rootroot00000000000000from __future__ import unicode_literals from decimal import Decimal from django.contrib.auth.models import AnonymousUser from django.db import transaction from django.test import TransactionTestCase, RequestFactory, TestCase import waffle from waffle.models import Switch, Sample from waffle.testutils import override_switch, override_flag, override_sample class OverrideSwitchMixin: def test_switch_existed_and_was_active(self): Switch.objects.create(name='foo', active=True) with override_switch('foo', active=True): assert waffle.switch_is_active('foo') with override_switch('foo', active=False): assert not waffle.switch_is_active('foo') # make sure it didn't change 'active' value assert Switch.objects.get(name='foo').active def test_switch_existed_and_was_NOT_active(self): Switch.objects.create(name='foo', active=False) with override_switch('foo', active=True): assert waffle.switch_is_active('foo') with override_switch('foo', active=False): assert not waffle.switch_is_active('foo') # make sure it didn't change 'active' value assert not Switch.objects.get(name='foo').active def test_new_switch(self): assert not Switch.objects.filter(name='foo').exists() with override_switch('foo', active=True): assert waffle.switch_is_active('foo') with override_switch('foo', active=False): assert not waffle.switch_is_active('foo') assert not Switch.objects.filter(name='foo').exists() def test_as_decorator(self): assert not Switch.objects.filter(name='foo').exists() @override_switch('foo', active=True) def test_enabled(): assert waffle.switch_is_active('foo') test_enabled() @override_switch('foo', active=False) def test_disabled(): assert not waffle.switch_is_active('foo') test_disabled() assert not Switch.objects.filter(name='foo').exists() def test_restores_after_exception(self): Switch.objects.create(name='foo', active=True) def inner(): with override_switch('foo', active=False): raise RuntimeError("Trying to break") with self.assertRaises(RuntimeError): inner() assert Switch.objects.get(name='foo').active def test_restores_after_exception_in_decorator(self): Switch.objects.create(name='foo', active=True) @override_switch('foo', active=False) def inner(): raise RuntimeError("Trying to break") with self.assertRaises(RuntimeError): inner() assert Switch.objects.get(name='foo').active def test_cache_is_flushed_by_testutils_even_in_transaction(self): Switch.objects.create(name='foo', active=True) with transaction.atomic(): with override_switch('foo', active=True): assert waffle.switch_is_active('foo') with override_switch('foo', active=False): assert not waffle.switch_is_active('foo') assert waffle.switch_is_active('foo') class OverrideSwitchTestCase(OverrideSwitchMixin, TestCase): """ Run tests with Django TestCase """ class OverrideSwitchTransactionTestCase(OverrideSwitchMixin, TransactionTestCase): """ Run tests with Django TransactionTestCase """ def req(): r = RequestFactory().get('/') r.user = AnonymousUser() return r class OverrideFlagTestsMixin: def test_flag_existed_and_was_active(self): waffle.get_waffle_flag_model().objects.create(name='foo', everyone=True) with override_flag('foo', active=True): assert waffle.flag_is_active(req(), 'foo') with override_flag('foo', active=False): assert not waffle.flag_is_active(req(), 'foo') assert waffle.get_waffle_flag_model().objects.get(name='foo').everyone def test_flag_existed_and_was_inactive(self): waffle.get_waffle_flag_model().objects.create(name='foo', everyone=False) with override_flag('foo', active=True): assert waffle.flag_is_active(req(), 'foo') with override_flag('foo', active=False): assert not waffle.flag_is_active(req(), 'foo') assert waffle.get_waffle_flag_model().objects.get(name='foo').everyone is False def test_flag_existed_and_was_null(self): waffle.get_waffle_flag_model().objects.create(name='foo', everyone=None) with override_flag('foo', active=True): assert waffle.flag_is_active(req(), 'foo') with override_flag('foo', active=False): assert not waffle.flag_is_active(req(), 'foo') assert waffle.get_waffle_flag_model().objects.get(name='foo').everyone is None def test_flag_did_not_exist(self): assert not waffle.get_waffle_flag_model().objects.filter(name='foo').exists() with override_flag('foo', active=True): assert waffle.flag_is_active(req(), 'foo') with override_flag('foo', active=False): assert not waffle.flag_is_active(req(), 'foo') assert not waffle.get_waffle_flag_model().objects.filter(name='foo').exists() def test_cache_is_flushed_by_testutils_even_in_transaction(self): waffle.get_waffle_flag_model().objects.create(name='foo', everyone=True) with transaction.atomic(): with override_flag('foo', active=True): assert waffle.flag_is_active(req(), 'foo') with override_flag('foo', active=False): assert not waffle.flag_is_active(req(), 'foo') assert waffle.flag_is_active(req(), 'foo') class OverrideFlagsTestCase(OverrideFlagTestsMixin, TestCase): """ Run tests with Django TestCase """ class OverrideFlagsTransactionTestCase(OverrideFlagTestsMixin, TransactionTestCase): """ Run tests with Django TransactionTestCase """ class OverrideSampleTestsMixin: def test_sample_existed_and_was_100(self): Sample.objects.create(name='foo', percent='100.0') with override_sample('foo', active=True): assert waffle.sample_is_active('foo') with override_sample('foo', active=False): assert not waffle.sample_is_active('foo') self.assertEquals(Decimal('100.0'), Sample.objects.get(name='foo').percent) def test_sample_existed_and_was_0(self): Sample.objects.create(name='foo', percent='0.0') with override_sample('foo', active=True): assert waffle.sample_is_active('foo') with override_sample('foo', active=False): assert not waffle.sample_is_active('foo') self.assertEquals(Decimal('0.0'), Sample.objects.get(name='foo').percent) def test_sample_existed_and_was_50(self): Sample.objects.create(name='foo', percent='50.0') with override_sample('foo', active=True): assert waffle.sample_is_active('foo') with override_sample('foo', active=False): assert not waffle.sample_is_active('foo') self.assertEquals(Decimal('50.0'), Sample.objects.get(name='foo').percent) def test_sample_did_not_exist(self): assert not Sample.objects.filter(name='foo').exists() with override_sample('foo', active=True): assert waffle.sample_is_active('foo') with override_sample('foo', active=False): assert not waffle.sample_is_active('foo') assert not Sample.objects.filter(name='foo').exists() def test_cache_is_flushed_by_testutils_even_in_transaction(self): Sample.objects.create(name='foo', percent='100.0') with transaction.atomic(): with override_sample('foo', active=True): assert waffle.sample_is_active('foo') with override_sample('foo', active=False): assert not waffle.sample_is_active('foo') assert waffle.sample_is_active('foo') class OverrideSampleTestCase(OverrideSampleTestsMixin, TestCase): """ Run tests with Django TestCase """ class OverrideSampleTransactionTestCase(OverrideSampleTestsMixin, TransactionTestCase): """ Run tests with Django TransactionTestCase """ class OverrideSwitchOnClassTestsMixin(object): @classmethod def setUpClass(cls): super(OverrideSwitchOnClassTestsMixin, cls).setUpClass() assert not Switch.objects.filter(name='foo').exists() Switch.objects.create(name='foo', active=True) def test_undecorated_method_is_set_properly_for_switch(self): self.assertFalse(waffle.switch_is_active('foo')) @override_switch('foo', active=False) class OverrideSwitchOnClassTestCase(OverrideSwitchOnClassTestsMixin, TestCase): """ Run tests with Django TestCase """ @override_switch('foo', active=False) class OverrideSwitchOnClassTransactionTestCase(OverrideSwitchOnClassTestsMixin, TransactionTestCase): """ Run tests with Django TransactionTestCase """ class OverrideFlagOnClassTestsMixin(object): @classmethod def setUpClass(cls): super(OverrideFlagOnClassTestsMixin, cls).setUpClass() assert not waffle.get_waffle_flag_model().objects.filter(name='foo').exists() waffle.get_waffle_flag_model().objects.create(name='foo', everyone=True) def test_undecorated_method_is_set_properly_for_flag(self): self.assertFalse(waffle.flag_is_active(req(), 'foo')) @override_flag('foo', active=False) class OverrideFlagOnClassTestCase(OverrideFlagOnClassTestsMixin, TestCase): """ Run tests with Django TestCase """ @override_flag('foo', active=False) class OverrideFlagOnClassTransactionTestCase(OverrideFlagOnClassTestsMixin, TransactionTestCase): """ Run tests with Django TransactionTestCase """ class OverrideSampleOnClassTestsMixin(object): @classmethod def setUpClass(cls): super(OverrideSampleOnClassTestsMixin, cls).setUpClass() assert not Sample.objects.filter(name='foo').exists() Sample.objects.create(name='foo', percent='100.0') def test_undecorated_method_is_set_properly_for_sample(self): self.assertFalse(waffle.sample_is_active('foo')) @override_sample('foo', active=False) class OverrideSampleOnClassTestCase(OverrideSampleOnClassTestsMixin, TestCase): """ Run tests with Django TestCase """ @override_sample('foo', active=False) class OverrideSampleOnClassTransactionTestCase(OverrideSampleOnClassTestsMixin, TransactionTestCase): """ Run tests with Django TransactionTestCase """ class InheritanceOverrideSwitchOnClassTests(OverrideSwitchOnClassTestCase): """ Extend ``OverrideSwitchOnClassTestCase`` and make sure ``override_switch`` change still works. """ def test_child_undecorated_method_is_set_properly_for_switch(self): self.assertFalse(waffle.switch_is_active('foo')) class InheritanceOverrideFlagOnClassTests(OverrideFlagOnClassTestCase): """ Extend ``OverrideFlagOnClassTestCase`` and make sure ``override_flag`` change still works. """ def test_child_undecorated_method_is_set_properly_for_flag(self): self.assertFalse(waffle.flag_is_active(req(), 'foo')) class InheritanceOverrideSampleOnClassTests(OverrideSampleOnClassTestCase): """ Extend ``OverrideSampleOnClassTestCase`` and make sure ``override_sample`` change still works. """ def test_child_undecorated_method_is_set_properly_for_sample(self): self.assertFalse(waffle.sample_is_active('foo')) django-waffle-0.20.0/waffle/tests/test_utils.py000066400000000000000000000013001362207625100214520ustar00rootroot00000000000000from __future__ import unicode_literals from django.conf import settings from django.test import TestCase from django.test.utils import override_settings from waffle import defaults from waffle.utils import get_setting class GetSettingTests(TestCase): def test_overridden_setting(self): prefix = get_setting('CACHE_PREFIX') self.assertEqual(settings.WAFFLE_CACHE_PREFIX, prefix) def test_default_setting(self): age = get_setting('MAX_AGE') self.assertEqual(defaults.MAX_AGE, age) def test_override_settings(self): assert not get_setting('OVERRIDE') with override_settings(WAFFLE_OVERRIDE=True): assert get_setting('OVERRIDE') django-waffle-0.20.0/waffle/tests/test_views.py000066400000000000000000000045141362207625100214610ustar00rootroot00000000000000from __future__ import unicode_literals from django.urls import reverse from waffle import get_waffle_flag_model from waffle.models import Sample, Switch from waffle.tests.base import TestCase class WaffleViewTests(TestCase): def test_wafflejs(self): response = self.client.get(reverse('wafflejs')) self.assertEqual(200, response.status_code) self.assertEqual('application/x-javascript', response['content-type']) cache_control = [control.strip() for control in response['cache-control'].split(',')] self.assertIn('max-age=0', cache_control) def test_flush_all_flags(self): """Test the 'FLAGS_ALL' list gets invalidated correctly.""" get_waffle_flag_model().objects.create(name='myflag1', everyone=True) response = self.client.get(reverse('wafflejs')) self.assertEqual(200, response.status_code) assert ('myflag1', True) in response.context['flags'] get_waffle_flag_model().objects.create(name='myflag2', everyone=True) response = self.client.get(reverse('wafflejs')) self.assertEqual(200, response.status_code) assert ('myflag2', True) in response.context['flags'] def test_flush_all_switches(self): """Test the 'SWITCHES_ALL' list gets invalidated correctly.""" switch = Switch.objects.create(name='myswitch', active=True) response = self.client.get(reverse('wafflejs')) self.assertEqual(200, response.status_code) assert ('myswitch', True) in response.context['switches'] switch.active = False switch.save() response = self.client.get(reverse('wafflejs')) self.assertEqual(200, response.status_code) assert ('myswitch', False) in response.context['switches'] def test_flush_all_samples(self): """Test the 'SAMPLES_ALL' list gets invalidated correctly.""" Sample.objects.create(name='sample1', percent='100.0') response = self.client.get(reverse('wafflejs')) self.assertEqual(200, response.status_code) assert ('sample1', True) in response.context['samples'] Sample.objects.create(name='sample2', percent='100.0') response = self.client.get(reverse('wafflejs')) self.assertEqual(200, response.status_code) assert ('sample2', True) in response.context['samples'] django-waffle-0.20.0/waffle/tests/test_waffle.py000066400000000000000000000627271362207625100216020ustar00rootroot00000000000000from __future__ import unicode_literals import logging import random import threading import unittest from django.contrib.auth import get_user_model from django.conf import settings from django.contrib.auth.models import AnonymousUser, Group from django.db import connection, transaction from django.test import RequestFactory, TransactionTestCase from django.test.utils import override_settings import mock import waffle from test_app import views from test_app.models import CompanyAwareFlag, Company from waffle.middleware import WaffleMiddleware from waffle.models import Sample, Switch from waffle.tests.base import TestCase DATABASES = {'default', 'readonly'} def get(**kw): request = RequestFactory().get('/foo', data=kw) request.user = AnonymousUser() return request def process_request(request, view): response = view(request) return WaffleMiddleware().process_response(request, response) class WaffleTests(TestCase): databases = DATABASES def assert_flag_dynamically_created_with_value(self, expected_value): FLAG_NAME = 'my_dynamically_created_flag' flag_model = waffle.get_waffle_flag_model() assert flag_model.objects.count() == 0 assert expected_value == waffle.flag_is_active(get(), FLAG_NAME) assert flag_model.objects.count() == 1 flag = flag_model.objects.get(name=FLAG_NAME) assert flag.name == FLAG_NAME return flag def test_persist_active_flag(self): waffle.get_waffle_flag_model().objects.create(name='myflag', percent='0.1') request = get() # Flag stays on. request.COOKIES['dwf_myflag'] = 'True' response = process_request(request, views.flag_in_view) self.assertEqual(b'on', response.content) assert 'dwf_myflag' in response.cookies self.assertEqual('True', response.cookies['dwf_myflag'].value) def test_persist_inactive_flag(self): waffle.get_waffle_flag_model().objects.create(name='myflag', percent='99.9') request = get() # Flag stays off. request.COOKIES['dwf_myflag'] = 'False' response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) assert 'dwf_myflag' in response.cookies self.assertEqual('False', response.cookies['dwf_myflag'].value) def test_no_set_unused_flag(self): """An unused flag shouldn't have its cookie reset.""" request = get() request.COOKIES['dwf_unused'] = 'True' response = process_request(request, views.flag_in_view) assert 'dwf_unused' not in response.cookies def test_superuser(self): """Test the superuser switch.""" waffle.get_waffle_flag_model().objects.create(name='myflag', superusers=True) request = get() response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) assert 'dwf_myflag' not in response.cookies superuser = get_user_model()(username='foo', is_superuser=True) request.user = superuser response = process_request(request, views.flag_in_view) self.assertEqual(b'on', response.content) assert 'dwf_myflag' not in response.cookies non_superuser = get_user_model()(username='bar', is_superuser=False) non_superuser.save() request.user = non_superuser response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) assert 'dwf_myflag' not in response.cookies def test_staff(self): """Test the staff switch.""" waffle.get_waffle_flag_model().objects.create(name='myflag', staff=True) request = get() response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) assert 'dwf_myflag' not in response.cookies staff = get_user_model()(username='foo', is_staff=True) request.user = staff response = process_request(request, views.flag_in_view) self.assertEqual(b'on', response.content) assert 'dwf_myflag' not in response.cookies non_staff = get_user_model()(username='foo', is_staff=False) non_staff.save() request.user = non_staff response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) assert 'dwf_myflag' not in response.cookies def test_languages(self): waffle.get_waffle_flag_model().objects.create(name='myflag', languages='en,fr') request = get() response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) request.LANGUAGE_CODE = 'en' response = process_request(request, views.flag_in_view) self.assertEqual(b'on', response.content) request.LANGUAGE_CODE = 'de' response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) def test_user(self): """Test the per-user switch.""" user = get_user_model().objects.create(username='foo') flag = waffle.get_waffle_flag_model().objects.create(name='myflag') flag.users.add(user) request = get() request.user = user response = process_request(request, views.flag_in_view) self.assertEqual(b'on', response.content) assert 'dwf_myflag' not in response.cookies request.user = get_user_model().objects.create(username='someone_else') response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) assert 'dwf_myflag' not in response.cookies # Unsetting the flag on a user should have an effect. flag.users.remove(user) request.user = user response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) def test_group(self): """Test the per-group switch.""" group = Group.objects.create(name='foo') user = get_user_model().objects.create(username='bar') user.groups.add(group) flag = waffle.get_waffle_flag_model().objects.create(name='myflag') flag.groups.add(group) request = get() request.user = user response = process_request(request, views.flag_in_view) self.assertEqual(b'on', response.content) assert 'dwf_myflag' not in response.cookies request.user = get_user_model()(username='someone_else') request.user.save() response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) assert 'dwf_myflag' not in response.cookies # Unsetting the flag on a group should have an effect. flag.groups.remove(group) request.user = user response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) def test_authenticated(self): """Test the authenticated/anonymous switch.""" waffle.get_waffle_flag_model().objects.create(name='myflag', authenticated=True) request = get() response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) assert 'dwf_myflag' not in response.cookies request.user = get_user_model()(username='foo') assert request.user.is_authenticated response = process_request(request, views.flag_in_view) self.assertEqual(b'on', response.content) assert 'dwf_myflag' not in response.cookies def test_everyone_on(self): """Test the 'everyone' switch on.""" waffle.get_waffle_flag_model().objects.create(name='myflag', everyone=True) request = get() request.COOKIES['dwf_myflag'] = 'False' response = process_request(request, views.flag_in_view) self.assertEqual(b'on', response.content) assert 'dwf_myflag' not in response.cookies request.user = get_user_model()(username='foo') assert request.user.is_authenticated response = process_request(request, views.flag_in_view) self.assertEqual(b'on', response.content) assert 'dwf_myflag' not in response.cookies def test_everyone_off(self): """Test the 'everyone' switch off.""" waffle.get_waffle_flag_model().objects.create(name='myflag', everyone=False, authenticated=True) request = get() request.COOKIES['dwf_myflag'] = 'True' response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) assert 'dwf_myflag' not in response.cookies request.user = get_user_model()(username='foo') assert request.user.is_authenticated response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) assert 'dwf_myflag' not in response.cookies def test_percent(self): """If you have no cookie, you get a cookie!""" waffle.get_waffle_flag_model().objects.create(name='myflag', percent='50.0') request = get() response = process_request(request, views.flag_in_view) assert 'dwf_myflag' in response.cookies @mock.patch.object(random, 'uniform') def test_reroll(self, uniform): """Even without a cookie, calling flag_is_active twice should return the same value.""" waffle.get_waffle_flag_model().objects.create(name='myflag', percent='50.0') # Make sure we're not really random. request = get() # Create a clean request. assert not hasattr(request, 'waffles') uniform.return_value = '10' # < 50. Flag is True. assert waffle.flag_is_active(request, 'myflag') assert hasattr(request, 'waffles') # We should record this flag. assert 'myflag' in request.waffles assert request.waffles['myflag'][0] uniform.return_value = '70' # > 50. Normally, Flag would be False. assert waffle.flag_is_active(request, 'myflag') assert request.waffles['myflag'][0] def test_undefined(self): """Undefined flags are always false.""" request = get() assert not waffle.flag_is_active(request, 'foo') @override_settings(WAFFLE_FLAG_DEFAULT=True) def test_undefined_default(self): """WAFFLE_FLAG_DEFAULT controls undefined flags.""" request = get() assert waffle.flag_is_active(request, 'foo') @override_settings(WAFFLE_OVERRIDE=True) def test_override(self): request = get(foo='1') waffle.get_waffle_flag_model().objects.create(name='foo') # Off for everyone. assert waffle.flag_is_active(request, 'foo') def test_testing_flag(self): waffle.get_waffle_flag_model().objects.create(name='foo', testing=True) request = get(dwft_foo='1') assert waffle.flag_is_active(request, 'foo') assert 'foo' in request.waffle_tests assert request.waffle_tests['foo'] # GET param should override cookie request = get(dwft_foo='0') request.COOKIES['dwft_foo'] = 'True' assert not waffle.flag_is_active(request, 'foo') assert 'foo' in request.waffle_tests assert not request.waffle_tests['foo'] def test_testing_disabled_flag(self): waffle.get_waffle_flag_model().objects.create(name='foo') request = get(dwft_foo='1') assert not waffle.flag_is_active(request, 'foo') assert not hasattr(request, 'waffle_tests') request = get(dwft_foo='0') assert not waffle.flag_is_active(request, 'foo') assert not hasattr(request, 'waffle_tests') def test_set_then_unset_testing_flag(self): waffle.get_waffle_flag_model().objects.create(name='myflag', testing=True) response = self.client.get('/flag_in_view?dwft_myflag=1') self.assertEqual(b'on', response.content) response = self.client.get('/flag_in_view') self.assertEqual(b'on', response.content) response = self.client.get('/flag_in_view?dwft_myflag=0') self.assertEqual(b'off', response.content) response = self.client.get('/flag_in_view') self.assertEqual(b'off', response.content) response = self.client.get('/flag_in_view?dwft_myflag=1') self.assertEqual(b'on', response.content) @override_settings(DATABASE_ROUTERS=['waffle.tests.base.ReplicationRouter']) def test_everyone_on_read_from_write_db(self): flag = waffle.get_waffle_flag_model().objects.create(name='myflag', everyone=True) request = get() response = process_request(request, views.flag_in_view) # By default, flag_is_active should hit whatever it configured as the # read DB (so values will be stale if replication is lagged). self.assertEqual(b'off', response.content) with override_settings(WAFFLE_READ_FROM_WRITE_DB=True): # Save the flag again to flush the cache. flag.save() # The next read should now be directed to the write DB, ensuring # the cache and DB are in sync. response = process_request(request, views.flag_in_view) self.assertEqual(b'on', response.content) @override_settings(WAFFLE_FLAG_MODEL='test_app.CompanyAwareFlag', AUTH_USER_MODEL='test_app.CompanyUser') def test_pluggable_model(self): flag_model = waffle.get_waffle_flag_model() self.assertEqual(CompanyAwareFlag, flag_model) acme_company = Company.objects.create(name='Acme Ltd.') feline_company = Company.objects.create(name='Feline LLC') acme_company_flag = waffle.get_waffle_flag_model().objects.create(name='myflag', superusers=True) acme_company_flag.companies.add(acme_company) request = get() response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) assert 'dwf_myflag' not in response.cookies acme_user = get_user_model()(username='acme.mcfield', company=acme_company) request.user = acme_user response = process_request(request, views.flag_in_view) self.assertEqual(b'on', response.content) assert 'dwf_myflag' not in response.cookies feline_user = get_user_model()(username='acme.mcfield', company=feline_company) request.user = feline_user response = process_request(request, views.flag_in_view) self.assertEqual(b'off', response.content) assert 'dwf_myflag' not in response.cookies @override_settings(WAFFLE_CREATE_MISSING_FLAGS=True) @override_settings(WAFFLE_FLAG_DEFAULT=False) def test_flag_created_dynamically_default_false(self): self.assert_flag_dynamically_created_with_value(False) @override_settings(WAFFLE_CREATE_MISSING_FLAGS=True) @override_settings(WAFFLE_FLAG_DEFAULT=True) def test_flag_created_dynamically_default_true(self): self.assert_flag_dynamically_created_with_value(True) @mock.patch('waffle.models.logger') def test_no_logging_missing_flag_by_default(self, mock_logger): request = get() waffle.flag_is_active(request, 'foo') mock_logger.log.call_count == 0 @override_settings(WAFFLE_LOG_MISSING_FLAGS=logging.WARNING) @mock.patch('waffle.models.logger') def test_logging_missing_flag(self, mock_logger): request = get() waffle.flag_is_active(request, 'foo') mock_logger.log.assert_called_with(logging.WARNING, 'Flag %s not found', 'foo') class SwitchTests(TestCase): databases = DATABASES def assert_switch_dynamically_created_with_value(self, expected_value): SWITCH_NAME = 'my_dynamically_created_switch' assert Switch.objects.count() == 0 assert expected_value == waffle.switch_is_active(SWITCH_NAME) assert Switch.objects.count() == 1 switch = Switch.objects.get(name=SWITCH_NAME) assert switch.name == SWITCH_NAME assert expected_value == switch.active def test_switch_active(self): switch = Switch.objects.create(name='myswitch', active=True) assert waffle.switch_is_active(switch.name) def test_switch_inactive(self): switch = Switch.objects.create(name='myswitch', active=False) assert not waffle.switch_is_active(switch.name) def test_switch_active_from_cache(self): """Do not make two queries for an existing active switch.""" switch = Switch.objects.create(name='myswitch', active=True) # Get the value once so that it will be put into the cache assert waffle.switch_is_active(switch.name) queries = len(connection.queries) assert waffle.switch_is_active(switch.name) self.assertEqual(queries, len(connection.queries)) def test_switch_inactive_from_cache(self): """Do not make two queries for an existing inactive switch.""" switch = Switch.objects.create(name='myswitch', active=False) # Get the value once so that it will be put into the cache assert not waffle.switch_is_active(switch.name) queries = len(connection.queries) assert not waffle.switch_is_active(switch.name) self.assertEqual(queries, len(connection.queries)) def test_undefined(self): assert not waffle.switch_is_active('foo') @override_settings(WAFFLE_SWITCH_DEFAULT=True) def test_undefined_default(self): assert waffle.switch_is_active('foo') @override_settings(DEBUG=True) def test_no_query(self): """Do not make two queries for a non-existent switch.""" assert not Switch.objects.filter(name='foo').exists() queries = len(connection.queries) assert not waffle.switch_is_active('foo') assert len(connection.queries) > queries queries = len(connection.queries) assert not waffle.switch_is_active('foo') self.assertEqual(queries, len(connection.queries)) @override_settings(DATABASE_ROUTERS=['waffle.tests.base.ReplicationRouter']) def test_read_from_write_db(self): switch = Switch.objects.create(name='switch', active=True) # By default, switch_is_active should hit whatever it configured as the # read DB (so values will be stale if replication is lagged). assert not waffle.switch_is_active(switch.name) with override_settings(WAFFLE_READ_FROM_WRITE_DB=True): # Save the switch again to flush the cache. switch.save() # The next read should now be directed to the write DB, ensuring # the cache and DB are in sync. assert waffle.switch_is_active(switch.name) @override_settings(WAFFLE_CREATE_MISSING_SWITCHES=True) @override_settings(WAFFLE_SWITCH_DEFAULT=False) def test_switch_created_dynamically_false(self): self.assert_switch_dynamically_created_with_value(False) @override_settings(WAFFLE_CREATE_MISSING_SWITCHES=True) @override_settings(WAFFLE_SWITCH_DEFAULT=True) def test_switch_created_dynamically_true(self): self.assert_switch_dynamically_created_with_value(True) @mock.patch('waffle.models.logger') def test_no_logging_missing_switch_by_default(self, mock_logger): waffle.switch_is_active('foo') mock_logger.log.call_count == 0 @override_settings(WAFFLE_LOG_MISSING_SWITCHES=logging.WARNING) @mock.patch('waffle.models.logger') def test_logging_missing_switch(self, mock_logger): waffle.switch_is_active('foo') mock_logger.log.assert_called_with(logging.WARNING, 'Switch %s not found', 'foo') class SampleTests(TestCase): databases = DATABASES def assert_sample_dynamically_created_with_value(self, is_active, expected_value): SAMPLE_NAME = 'my_dynamically_created_sample' assert Sample.objects.count() == 0 assert is_active == waffle.sample_is_active(SAMPLE_NAME) assert Sample.objects.count() == 1 sample = Sample.objects.get(name=SAMPLE_NAME) assert sample.name == SAMPLE_NAME assert sample.percent == expected_value def test_sample_100(self): sample = Sample.objects.create(name='sample', percent='100.0') assert waffle.sample_is_active(sample.name) def test_sample_0(self): sample = Sample.objects.create(name='sample', percent='0.0') assert not waffle.sample_is_active(sample.name) def test_undefined(self): assert not waffle.sample_is_active('foo') @override_settings(WAFFLE_SAMPLE_DEFAULT=True) def test_undefined_default(self): assert waffle.sample_is_active('foo') @override_settings(DATABASE_ROUTERS=['waffle.tests.base.ReplicationRouter']) def test_read_from_write_db(self): sample = Sample.objects.create(name='sample', percent='100.0') # By default, sample_is_active should hit whatever it configured as the # read DB (so values will be stale if replication is lagged). assert not waffle.sample_is_active(sample.name) with override_settings(WAFFLE_READ_FROM_WRITE_DB=True): # Save the sample again to flush the cache. sample.save() # The next read should now be directed to the write DB, ensuring # the cache and DB are in sync. assert waffle.sample_is_active(sample.name) @override_settings(WAFFLE_CREATE_MISSING_SAMPLES=True) @override_settings(WAFFLE_SAMPLE_DEFAULT=False) def test_sample_created_dynamically_default_false(self): self.assert_sample_dynamically_created_with_value(False, 0.0) @override_settings(WAFFLE_CREATE_MISSING_SAMPLES=True) @override_settings(WAFFLE_SAMPLE_DEFAULT=True) def test_sample_created_dynamically_default_true(self): self.assert_sample_dynamically_created_with_value(True, 100.0) @mock.patch('waffle.models.logger') def test_no_logging_missing_sample_by_default(self, mock_logger): waffle.switch_is_active('foo') mock_logger.log.call_count == 0 @override_settings(WAFFLE_LOG_MISSING_SAMPLES=logging.WARNING) @mock.patch('waffle.models.logger') def test_logging_missing_sample(self, mock_logger): waffle.sample_is_active('foo') mock_logger.log.assert_called_with(logging.WARNING, 'Sample %s not found', 'foo') class TransactionTestMixin(object): """Mixin providing an abstract test case for writing in a transaction. """ def create_toggle(self): """Create an inactive feature toggle (i.e. flag, switch, sample).""" raise NotImplementedError def flip_toggle(self, toggle): """Flip the toggle to an active state.""" raise NotImplementedError def toggle_is_active(self, toggle): """Use the built-in *_is_active helper to check the toggle's state.""" raise NotImplementedError @unittest.skipIf('sqlite3' in settings.DATABASES['default']['ENGINE'], 'This test uses threads, which the sqlite3 DB engine ' 'does not support.') def test_flip_toggle_in_transaction(self): """Wait to invalidate the cache until after the current transaction. This test covers a potential race condition where, if the cache were flushed in the middle of a transaction, the next read from the database (before the transaction is committed) would get a stale value and cache it. See #296 for more context. """ toggle = self.create_toggle() self.addCleanup(toggle.delete) written_in_background_thread = threading.Event() read_in_main_thread = threading.Event() @transaction.atomic def update_toggle(): self.flip_toggle(toggle) # Signal to the main thread that the toggle has been updated, but # the transaction is not yet committed. written_in_background_thread.set() # Pause here to allow the main thread to make an assertion. read_in_main_thread.wait(timeout=1) # Start a background thread to update the toggle in a transaction. t = threading.Thread(target=update_toggle) t.daemon = True t.start() # After the toggle is updated but before the transaction is committed, # the cache will still have the previous value. written_in_background_thread.wait(timeout=1) assert not self.toggle_is_active(toggle) # After the transaction is committed, the cache should have been # invalidated, hence the next call to *_is_active should have the # correct value. read_in_main_thread.set() t.join(timeout=1) assert self.toggle_is_active(toggle) class FlagTransactionTests(TransactionTestMixin, TransactionTestCase): def create_toggle(self): return waffle.get_waffle_flag_model().objects.create( name='transaction-flag-name', everyone=False ) def flip_toggle(self, flag): flag.everyone = True flag.save() def toggle_is_active(self, flag): return waffle.flag_is_active(get(), flag.name) class SwitchTransactionTests(TransactionTestMixin, TransactionTestCase): def create_toggle(self): return Switch.objects.create(name='transaction-switch-name', active=False) def flip_toggle(self, switch): switch.active = True switch.save() def toggle_is_active(self, switch): return waffle.switch_is_active(switch.name) class SampleTransactionTests(TransactionTestMixin, TransactionTestCase): def create_toggle(self): return Sample.objects.create(name='transaction-sample-name', percent=0) def flip_toggle(self, sample): sample.percent = 100 sample.save() def toggle_is_active(self, sample): return waffle.sample_is_active(sample.name) django-waffle-0.20.0/waffle/testutils.py000066400000000000000000000055721362207625100201700ustar00rootroot00000000000000from __future__ import unicode_literals from django.test.utils import TestContextDecorator from waffle import get_waffle_flag_model from waffle.models import Switch, Sample __all__ = ['override_flag', 'override_sample', 'override_switch'] class _overrider(TestContextDecorator): def __init__(self, name, active): super(_overrider, self).__init__() self.name = name self.active = active def get(self): self.obj, self.created = self.cls.objects.get_or_create(name=self.name) def update(self, active): raise NotImplementedError def get_value(self): raise NotImplementedError def enable(self): self.get() self.old_value = self.get_value() if self.old_value != self.active: self.update(self.active) def disable(self): if self.created: self.obj.delete() self.obj.flush() else: self.update(self.old_value) class override_switch(_overrider): """ override_switch is a contextmanager for easier testing of switches. It accepts two parameters, name of the switch and it's state. Example usage:: with override_switch('happy_mode', active=True): ... If `Switch` already existed, it's value would be changed inside the context block, then restored to the original value. If `Switch` did not exist before entering the context, it is created, then removed at the end of the block. It can also act as a decorator:: @override_switch('happy_mode', active=True) def test_happy_mode_enabled(): ... """ cls = Switch def update(self, active): obj = self.cls.objects.get(pk=self.obj.pk) obj.active = active obj.save() obj.flush() def get_value(self): return self.obj.active class override_flag(_overrider): cls = get_waffle_flag_model() def update(self, active): obj = self.cls.objects.get(pk=self.obj.pk) obj.everyone = active obj.save() obj.flush() def get_value(self): return self.obj.everyone class override_sample(_overrider): cls = Sample def get(self): try: self.obj = self.cls.objects.get(name=self.name) self.created = False except self.cls.DoesNotExist: self.obj = self.cls.objects.create(name=self.name, percent='0.0') self.created = True def update(self, active): if active is True: p = 100.0 elif active is False: p = 0.0 else: p = active obj = self.cls.objects.get(pk=self.obj.pk) obj.percent = '{0}'.format(p) obj.save() obj.flush() def get_value(self): p = self.obj.percent if p == 100.0: return True if p == 0.0: return False return p django-waffle-0.20.0/waffle/urls.py000066400000000000000000000002631362207625100171050ustar00rootroot00000000000000from __future__ import unicode_literals from django.conf.urls import url from waffle.views import wafflejs urlpatterns = [ url(r'^wafflejs$', wafflejs, name='wafflejs'), ] django-waffle-0.20.0/waffle/utils.py000066400000000000000000000012501362207625100172550ustar00rootroot00000000000000from __future__ import unicode_literals, absolute_import import hashlib from django.conf import settings from django.core.cache import caches import waffle from waffle import defaults def get_setting(name, default=None): try: return getattr(settings, 'WAFFLE_' + name) except AttributeError: return getattr(defaults, name, default) def keyfmt(k, v=None): prefix = get_setting('CACHE_PREFIX') + waffle.__version__ if v is None: key = prefix + k else: key = prefix + hashlib.md5((k % v).encode('utf-8')).hexdigest() return key def get_cache(): CACHE_NAME = get_setting('CACHE_NAME') return caches[CACHE_NAME] django-waffle-0.20.0/waffle/views.py000066400000000000000000000021331362207625100172530ustar00rootroot00000000000000from __future__ import unicode_literals from django.http import HttpResponse from django.template import loader from django.views.decorators.cache import never_cache from waffle import get_waffle_flag_model from waffle.models import Sample, Switch from waffle.utils import get_setting @never_cache def wafflejs(request): return HttpResponse(_generate_waffle_js(request), content_type='application/x-javascript') def _generate_waffle_js(request): flags = get_waffle_flag_model().get_all() flag_values = [(f.name, f.is_active(request)) for f in flags] switches = Switch.get_all() switch_values = [(s.name, s.is_active()) for s in switches] samples = Sample.get_all() sample_values = [(s.name, s.is_active()) for s in samples] return loader.render_to_string('waffle/waffle.js', { 'flags': flag_values, 'switches': switch_values, 'samples': sample_values, 'flag_default': get_setting('FLAG_DEFAULT'), 'switch_default': get_setting('SWITCH_DEFAULT'), 'sample_default': get_setting('SAMPLE_DEFAULT'), })