├── .arcconfig ├── .gitignore ├── .travis.yml ├── CHANGES ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── TODO.md ├── example_project ├── .gitignore ├── __init__.py ├── add_switches.py ├── app │ ├── __init__.py │ └── gutter.py ├── arguments.py ├── gutter.db ├── manage.py ├── settings.py ├── templates │ ├── 404.html │ └── 500.html └── urls.py ├── gutter-django.sublime-project ├── gutter ├── __init__.py └── django │ ├── __init__.py │ ├── autodiscovery.py │ ├── forms.py │ ├── helpers.py │ ├── media │ ├── coffee │ │ └── gutter.coffee │ ├── config.rb │ ├── css │ │ └── gutter.css │ ├── img │ │ ├── button-bg.jpg │ │ ├── delete.png │ │ └── edit.png │ ├── js │ │ ├── gutter.js │ │ └── string_score.min.js │ └── sass │ │ └── gutter.scss │ ├── nexus_modules.py │ ├── registry.py │ ├── templates │ └── gutter │ │ ├── index.html │ │ ├── nexus │ │ └── dashboard.html │ │ ├── switch.html │ │ └── switch_block.html │ └── templatetags │ ├── __init__.py │ └── gutter_helpers.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── fixtures.py ├── mixins.py ├── test_autodiscovery.py ├── test_django.py ├── test_forms.py ├── test_nexus_modules.py └── test_registry.py └── tox.ini /.arcconfig: -------------------------------------------------------------------------------- 1 | { 2 | "arcanist_configuration": "DisqusConfiguration", 3 | "base": "git:merge-base(origin/master), arc:upstream, git:HEAD^", 4 | "conduit_uri": "http://phabricator.local.disqus.net/", 5 | "copyright_holder": "Disqus, Inc.", 6 | "differential.field-selector": "DisqusDifferentialFieldSelector", 7 | "immutable_history": false, 8 | "lint.flake8.options": "--ignore=W391,W292,W293,E501,E225,E121,E123,E124,E127,E128,F999,F821 --exclude=disqus/contrib/*", 9 | "lint.jshint.bin": "jshint", 10 | "lint.jshint.prefix": "node_modules/jshint/bin", 11 | "lint.pep8.options": "--ignore=W391,W292,W293,E501,E225,W602", 12 | "phutil_libraries": { 13 | "disqus": "/usr/local/include/php/libdisqus/src" 14 | }, 15 | "project_id": "gutter", 16 | "jenkins.uri": "http://ci.local.disqus.net", 17 | "jenkins.job": "services-phabricator", 18 | "coverage": false 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info/ 3 | .DS_Store 4 | dist/ 5 | build/ 6 | *.egg/ 7 | .arcconfig 8 | .sass-cache 9 | *.sublime-workspace 10 | *.egg 11 | .*.sw* 12 | tags 13 | .eggs/ 14 | .tox/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | services: 4 | - redis-server 5 | 6 | python: 7 | - "2.7" 8 | - "pypy" 9 | 10 | env: 11 | - DJANGO=1.4.22 12 | - DJANGO=1.7.10 13 | - DJANGO=1.8.4 14 | 15 | install: 16 | - pip install Django==$DJANGO 17 | - python setup.py develop 18 | 19 | script: make test 20 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 0.7.2 2 | 3 | - Correct issue with trying to serialize datetime objects. 4 | 5 | 0.7.1 6 | 7 | - Changed the behavior of gutter.testutils.switches to monkey patch 8 | the is_active method which should solve scenarios where switches 9 | are reloaded during the context. 10 | 11 | 0.7.0 12 | 13 | - Added confirmation message for enabling switches globally. 14 | - Added date modified and sorts for switches on index view. 15 | 16 | 0.6.1 17 | 18 | - Require Nexus >= 0.2.0 19 | 20 | 0.6.0 21 | 22 | - Added basic switch inheritance. 23 | - Added auto collapsing of switch details in interface. 24 | - Added simple search filtering of switches in interface. 25 | 26 | 0.5.2 27 | 28 | - Improved display of Gutter dashboard widget. 29 | 30 | 0.5.1 31 | 32 | - Fixed switch_condition_removed signal to pass ``switch`` instance. 33 | 34 | 0.5.0 35 | 36 | - Updated signals to pass more useful information in each one (including the switch). 37 | 38 | 0.4.0 39 | 40 | - The percent field is now available on all ModelConditionSet's by default. 41 | - Fixed a CSRF conflict issue with Nexus. 42 | 43 | 0.3.0 44 | 45 | - Added gutter.testutils.with_switches decorator 46 | - Added gutter.testutils.SwitchContextManager 47 | 48 | 0.2.4 49 | 50 | - Updated autodiscovery code to resemble Django's newer example 51 | - Updated django-modeldict to 1.1.6 to solve a threading issue with registration 52 | - Added GARGOYLE_AUTO_CREATE setting to disable auto creation of new switches 53 | - Added the ability to pass arbitrary objects to the ifswitch template tag. 54 | 55 | 0.2.3 56 | 57 | - Ensure HostConditionSet is registered 58 | 59 | 0.2.2 60 | 61 | - Moved tests outside of gutter namespace 62 | 63 | 0.2.1 64 | 65 | - UI tweaks 66 | 67 | 0.2.0 68 | 69 | - [Backwards Incompatible] SELECTIVE switches without conditions are now inactive 70 | - Added ConditionSet.has_active_condition, and support for default NoneType instances 71 | for global / environment checks. 72 | - Added HostConditionSet which allows you to specify a switch for a single 73 | server hostname -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | group :media do 4 | gem 'compass' 5 | end 6 | 7 | group :development do 8 | gem 'guard' 9 | gem 'rb-readline' 10 | gem 'rb-fsevent', :require => false 11 | gem 'guard-shell' 12 | gem 'guard-compass' 13 | gem 'guard-coffeescript' 14 | gem 'guard-livereload' 15 | gem 'rake' 16 | end 17 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | addressable (2.3.2) 5 | chunky_png (1.2.6) 6 | coderay (1.0.8) 7 | coffee-script (2.2.0) 8 | coffee-script-source 9 | execjs 10 | coffee-script-source (1.4.0) 11 | compass (0.12.2) 12 | chunky_png (~> 1.2) 13 | fssm (>= 0.2.7) 14 | sass (~> 3.1) 15 | em-websocket (0.3.8) 16 | addressable (>= 2.1.1) 17 | eventmachine (>= 0.12.9) 18 | eventmachine (1.0.0) 19 | execjs (1.4.0) 20 | multi_json (~> 1.0) 21 | fssm (0.2.9) 22 | guard (1.5.4) 23 | listen (>= 0.4.2) 24 | lumberjack (>= 1.0.2) 25 | pry (>= 0.9.10) 26 | thor (>= 0.14.6) 27 | guard-coffeescript (1.2.1) 28 | coffee-script (>= 2.2.0) 29 | guard (>= 1.1.0) 30 | guard-compass (0.0.6) 31 | compass (>= 0.10.5) 32 | guard (>= 0.2.1) 33 | guard-livereload (1.1.2) 34 | em-websocket (>= 0.2.0) 35 | guard (>= 1.5.0) 36 | multi_json (~> 1.0) 37 | guard-shell (0.5.1) 38 | guard (>= 1.1.0) 39 | listen (0.6.0) 40 | lumberjack (1.0.2) 41 | method_source (0.8.1) 42 | multi_json (1.4.0) 43 | pry (0.9.10) 44 | coderay (~> 1.0.5) 45 | method_source (~> 0.8) 46 | slop (~> 3.3.1) 47 | rake (0.9.2.2) 48 | rb-fsevent (0.9.2) 49 | rb-readline (0.4.2) 50 | sass (3.2.3) 51 | slop (3.3.3) 52 | thor (0.16.0) 53 | 54 | PLATFORMS 55 | ruby 56 | 57 | DEPENDENCIES 58 | compass 59 | guard 60 | guard-coffeescript 61 | guard-compass 62 | guard-livereload 63 | guard-shell 64 | rake 65 | rb-fsevent 66 | rb-readline 67 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :shell do 2 | watch(/^(tests|gutter)(.*)\.py$/) do |match| 3 | puts `python setup.py nosetests` 4 | end 5 | end 6 | 7 | guard 'compass', project_path: 'gutter/django/media', configuration_file: 'gutter/django/media/config.rb' do 8 | watch(%r{.*scss}) 9 | end 10 | 11 | guard 'coffeescript', input: 'gutter/django/media/coffee', output: 'gutter/django/media/js' 12 | 13 | guard 'livereload' do 14 | watch(%r{media/.+\.(css|js)}) 15 | watch(%r{templates/.+\.(html)}) 16 | watch(%r{gutter/.+\.(py)}) 17 | end 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2010 DISQUS 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup.py README.rst MANIFEST.in LICENSE 2 | recursive-include gutter/django/templates/gutter * 3 | recursive-include gutter/django/media *.css *.js 4 | recursive-include gutter/django/media/img * 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = $(shell python setup.py --version) 2 | 3 | test: 4 | python setup.py nosetests 5 | 6 | release: 7 | git tag $(VERSION) 8 | git push origin $(VERSION) 9 | git push origin master 10 | python setup.py sdist bdist_wheel upload 11 | 12 | watch: 13 | bundle exec guard 14 | 15 | run: 16 | cd example_project && python manage.py runserver 17 | 18 | .PHONY: test release watch run 19 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Gutter-Django 2 | ------------- 3 | 4 | Gutter-Django is the Django-integration with Gutter. It includes a Nexus admin module, as well as common Django switch arguments. 5 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * Add code to expand/collapse switch rows for editing -------------------------------------------------------------------------------- /example_project/.gitignore: -------------------------------------------------------------------------------- 1 | sentry_index/ -------------------------------------------------------------------------------- /example_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disqus/gutter-django/771b557c784ed6b117615bec02829404f91e13a7/example_project/__init__.py -------------------------------------------------------------------------------- /example_project/add_switches.py: -------------------------------------------------------------------------------- 1 | from gutter.client.models import Switch, Condition 2 | from gutter.client.operators.misc import Percent 3 | from gutter.client.operators.comparable import Equals, MoreThan 4 | import gutter.client.settings 5 | 6 | from redis import Redis 7 | from durabledict.redis import RedisDict 8 | 9 | import gutter.django 10 | 11 | 12 | from arguments import User, Request 13 | 14 | 15 | # Configure Gutter 16 | gutter.client.settings.manager.storage_engine = RedisDict('gutter', Redis()) 17 | 18 | # Import the manager 19 | from gutter.client.default import gutter as manager 20 | 21 | 22 | switch = Switch('cool_feature', label='A cool feature', description='Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.') 23 | 24 | condition = Condition(User, 'name', Equals(value='Jeff')) 25 | switch.conditions.append(condition) 26 | 27 | condition = Condition(User, 'age', MoreThan(lower_limit=21)) 28 | switch.conditions.append(condition) 29 | 30 | manager.register(switch) 31 | 32 | switch = Switch('other_neat_feature', label='A neat additional feature', description='Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.') 33 | 34 | condition = Condition(Request, 'ip', Percent(percentage=10)) 35 | switch.conditions.append(condition) 36 | 37 | manager.register(switch) 38 | 39 | for switch in manager.switches: 40 | print '+', switch 41 | 42 | print type(manager.storage) 43 | -------------------------------------------------------------------------------- /example_project/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disqus/gutter-django/771b557c784ed6b117615bec02829404f91e13a7/example_project/app/__init__.py -------------------------------------------------------------------------------- /example_project/app/gutter.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from gutter.django import registry 4 | 5 | from gutter.client import arguments 6 | 7 | 8 | class User(arguments.Container): 9 | name = arguments.String(lambda self: 'Jeff') 10 | age = arguments.Value(lambda self: 29) 11 | registered_on = arguments.Boolean(lambda self: True) 12 | 13 | 14 | class Request(arguments.Container): 15 | ip = arguments.String('192.168.0.1') 16 | 17 | 18 | registry.arguments.register(User.name) 19 | registry.arguments.register(User.age) 20 | registry.arguments.register(User.registered_on) 21 | registry.arguments.register(Request.ip) 22 | -------------------------------------------------------------------------------- /example_project/arguments.py: -------------------------------------------------------------------------------- 1 | from gutter.client.arguments import Container 2 | 3 | 4 | class User(Container): 5 | 6 | def name(self): 7 | return 'Jeff' 8 | 9 | def age(self): 10 | return 28 11 | 12 | 13 | class Request(Container): 14 | 15 | def ip(self): 16 | return '192.168.0.1' 17 | -------------------------------------------------------------------------------- /example_project/gutter.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disqus/gutter-django/771b557c784ed6b117615bec02829404f91e13a7/example_project/gutter.db -------------------------------------------------------------------------------- /example_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | 4 | try: 5 | import settings # Assumed to be in the same directory. 6 | except ImportError: 7 | import sys 8 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 9 | sys.exit(1) 10 | 11 | if __name__ == "__main__": 12 | execute_manager(settings) 13 | -------------------------------------------------------------------------------- /example_project/settings.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import sys 3 | # Django settings for example_project project. 4 | 5 | DEBUG = True 6 | TEMPLATE_DEBUG = True 7 | 8 | ADMINS = ( 9 | # ('Your Name', 'your_email@domain.com'), 10 | ) 11 | 12 | INTERNAL_IPS = ('127.0.0.1',) 13 | 14 | MANAGERS = ADMINS 15 | 16 | PROJECT_ROOT = os.path.dirname(__file__) 17 | 18 | sys.path.insert(0, os.path.abspath(os.path.join(PROJECT_ROOT, '..'))) 19 | 20 | DATABASES = { 21 | 'default': { 22 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 23 | 'NAME': 'gutter.db', # Or path to database file if using sqlite3. 24 | 'USER': '', # Not used with sqlite3. 25 | 'PASSWORD': '', # Not used with sqlite3. 26 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 27 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 28 | } 29 | } 30 | 31 | # Local time zone for this installation. Choices can be found here: 32 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 33 | # although not all choices may be available on all operating systems. 34 | # On Unix systems, a value of None will cause Django to use the same 35 | # timezone as the operating system. 36 | # If running in a Windows environment this must be set to the same as your 37 | # system time zone. 38 | TIME_ZONE = 'America/Los_Angeles' 39 | 40 | # Language code for this installation. All choices can be found here: 41 | # http://www.i18nguy.com/unicode/language-identifiers.html 42 | LANGUAGE_CODE = 'en-us' 43 | 44 | SITE_ID = 1 45 | 46 | # If you set this to False, Django will make some optimizations so as not 47 | # to load the internationalization machinery. 48 | USE_I18N = True 49 | 50 | # If you set this to False, Django will not format dates, numbers and 51 | # calendars according to the current locale 52 | USE_L10N = True 53 | 54 | # Absolute path to the directory that holds media. 55 | # Example: "/home/media/media.lawrence.com/" 56 | MEDIA_ROOT = '' 57 | 58 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 59 | # trailing slash if there is a path component (optional in other cases). 60 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 61 | MEDIA_URL = '' 62 | 63 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 64 | # trailing slash. 65 | # Examples: "http://foo.com/media/", "/media/". 66 | ADMIN_MEDIA_PREFIX = '/admin/media/' 67 | 68 | # Make this unique, and don't share it with anybody. 69 | SECRET_KEY = ')*)&8a36)6%74e@-ne5(-!8a(vv#tkv)(eyg&@0=zd^pl!7=y@' 70 | 71 | # List of callables that know how to import templates from various sources. 72 | TEMPLATE_LOADERS = ( 73 | 'django.template.loaders.filesystem.Loader', 74 | 'django.template.loaders.app_directories.Loader', 75 | # 'django.template.loaders.eggs.Loader', 76 | ) 77 | 78 | MIDDLEWARE_CLASSES = ( 79 | 'django.middleware.common.CommonMiddleware', 80 | 'django.contrib.sessions.middleware.SessionMiddleware', 81 | 'django.middleware.csrf.CsrfViewMiddleware', 82 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 83 | 'django.contrib.messages.middleware.MessageMiddleware', 84 | ) 85 | 86 | ROOT_URLCONF = 'example_project.urls' 87 | 88 | TEMPLATE_DIRS = ( 89 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 90 | # Always use forward slashes, even on Windows. 91 | # Don't forget to use absolute paths, not relative paths. 92 | os.path.join(PROJECT_ROOT, 'templates'), 93 | ) 94 | 95 | NEXUS_MEDIA_PREFIX = '/media/' 96 | 97 | INSTALLED_APPS = ( 98 | 'django.contrib.auth', 99 | 'django.contrib.contenttypes', 100 | 'django.contrib.sessions', 101 | 'nexus', 102 | 'gutter.django', 103 | 'south', 104 | 'app' 105 | # Uncomment the next line to enable the admin: 106 | # 'django.contrib.admin', 107 | ) 108 | 109 | try: 110 | from local_settings import * 111 | except ImportError, e: 112 | print e 113 | 114 | TEMPLATE_STRING_IF_INVALID = "He's dead Jim! [%s]" -------------------------------------------------------------------------------- /example_project/templates/404.html: -------------------------------------------------------------------------------- 1 | 404 error -------------------------------------------------------------------------------- /example_project/templates/500.html: -------------------------------------------------------------------------------- 1 | 500 error -------------------------------------------------------------------------------- /example_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, patterns, url 2 | 3 | import nexus 4 | 5 | from redis import Redis 6 | 7 | from durabledict.redis import RedisDict 8 | 9 | import gutter.client.settings 10 | 11 | import gutter.django 12 | 13 | 14 | # Configure Gutter 15 | gutter.client.settings.manager.storage_engine = RedisDict(keyspace='gutter', connection=Redis()) 16 | gutter.django.autodiscover() 17 | 18 | # Configure Nexus 19 | nexus.autodiscover() 20 | 21 | urlpatterns = patterns('', 22 | url(r'', include(nexus.site.urls)), 23 | ) 24 | -------------------------------------------------------------------------------- /gutter-django.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "folder_exclude_patterns": [ 5 | "*.egg", 6 | "build", 7 | "dist", 8 | "*.egg-info", 9 | "doc/_*", 10 | ".ropeproject", 11 | "*.sass-cache" 12 | ], 13 | "file_exclude_patterns": [ 14 | "*.egg", 15 | "*.sublime-workspace", 16 | "*_pb2.py" 17 | ], 18 | "path": "." 19 | } 20 | ], 21 | "settings": { 22 | "tab_size": 4, 23 | "translate_tabs_to_spaces": true, 24 | "trim_trailing_white_space_on_save": true, 25 | "python_interpreter": "~/.virtualenvs/tempest/bin/python", 26 | "pyflakes_linting": true, 27 | "python_test_runner": { 28 | "before_test": "source ~/.virtualenvs/tempest/bin/activate", 29 | "after_test": "deactivate", 30 | "test_command": "./manage.py test --settings=tests.settings --noinput " 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /gutter/__init__.py: -------------------------------------------------------------------------------- 1 | __import__('pkg_resources').declare_namespace(__name__) -------------------------------------------------------------------------------- /gutter/django/__init__.py: -------------------------------------------------------------------------------- 1 | from autodiscovery import discover as autodiscover -------------------------------------------------------------------------------- /gutter/django/autodiscovery.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | def discover(): 8 | """ 9 | Auto-discover any Gutter configuration present in the django 10 | INSTALLED_APPS. 11 | """ 12 | from django.conf import settings 13 | from django.utils.importlib import import_module 14 | 15 | for app in settings.INSTALLED_APPS: 16 | module = '%s.gutter' % app 17 | 18 | try: 19 | import_module(module) 20 | logger.info('Successfully autodiscovered %s' % module) 21 | except: 22 | pass 23 | -------------------------------------------------------------------------------- /gutter/django/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.core.validators import RegexValidator 3 | from django.forms.formsets import formset_factory, BaseFormSet 4 | from django.forms.widgets import Select, Textarea 5 | from django.utils.html import escape, conditional_escape 6 | from django.utils.encoding import force_unicode 7 | 8 | from itertools import chain 9 | 10 | from gutter.django.registry import operators, arguments 11 | from gutter.client.models import Switch, Condition 12 | 13 | from functools import partial 14 | 15 | 16 | class OperatorSelectWidget(Select): 17 | 18 | def __init__(self, arguments, *args, **kwargs): 19 | self.arguments = arguments 20 | super(OperatorSelectWidget, self).__init__(*args, **kwargs) 21 | 22 | def render_options(self, choices, selected_choices): 23 | def render_option(option_value, option_label): 24 | option_value = force_unicode(option_value) 25 | selected_html = (option_value in selected_choices) and u' selected="selected"' or '' 26 | return u'' % ( 27 | ','.join(self.arguments[option_value]), 28 | escape(option_value), selected_html, 29 | conditional_escape(force_unicode(option_label))) 30 | 31 | # Normalize to strings. 32 | selected_choices = set([force_unicode(v) for v in selected_choices]) 33 | output = [] 34 | 35 | for option_value, option_label in chain(self.choices, choices): 36 | if isinstance(option_label, (list, tuple)): 37 | output.append(u'' % escape(force_unicode(option_value))) 38 | for option in option_label: 39 | output.append(render_option(*option)) 40 | output.append(u'') 41 | else: 42 | output.append(render_option(option_value, option_label)) 43 | return u'\n'.join(output) 44 | 45 | 46 | class SwitchForm(forms.Form): 47 | 48 | STATES = {1: 'Disabled', 2: 'Selective', 3: 'Global'}.items() 49 | SWITCH_NAME_REGEX_VALIDATOR = RegexValidator( 50 | regex=r'^[\w_:]+$', 51 | message='Must only be alphanumeric, underscore, and colon characters.' 52 | ) 53 | 54 | name = forms.CharField(max_length=100) 55 | label = forms.CharField(required=False) 56 | description = forms.CharField(widget=Textarea(), required=False) 57 | state = forms.IntegerField(widget=Select(choices=STATES)) 58 | 59 | compounded = forms.BooleanField(required=False) 60 | concent = forms.BooleanField(required=False) 61 | 62 | delete = forms.BooleanField(required=False) 63 | 64 | name.validators.append(SWITCH_NAME_REGEX_VALIDATOR) 65 | 66 | @classmethod 67 | def from_object(cls, switch): 68 | data = dict( 69 | label=switch.label, 70 | name=switch.name, 71 | description=switch.description, 72 | state=switch.state, 73 | compounded=switch.compounded, 74 | concent=switch.concent 75 | ) 76 | 77 | instance = cls(initial=data) 78 | 79 | condition_dicts = map(ConditionForm.to_dict, switch.conditions) 80 | instance.conditions = ConditionFormSet(initial=condition_dicts) 81 | instance.fields['name'].widget.attrs['readonly'] = True 82 | 83 | return instance 84 | 85 | def field(self, key): 86 | return self.data.get(key, None) or self.initial[key] 87 | 88 | @property 89 | def to_object(self): 90 | switch = Switch( 91 | name=self.cleaned_data['name'], 92 | label=self.cleaned_data['label'], 93 | description=self.cleaned_data['description'], 94 | state=self.cleaned_data['state'], 95 | compounded=self.cleaned_data['compounded'], 96 | concent=self.cleaned_data['concent'], 97 | ) 98 | 99 | return switch 100 | 101 | 102 | class ConditionForm(forms.Form): 103 | 104 | negative_widget = Select(choices=((False, 'Is'), (True, 'Is Not'))) 105 | 106 | argument = forms.ChoiceField(choices=arguments.as_choices) 107 | negative = forms.BooleanField(widget=negative_widget, required=False) 108 | operator = forms.ChoiceField( 109 | choices=operators.as_choices, 110 | widget=OperatorSelectWidget(operators.arguments) 111 | ) 112 | 113 | @staticmethod 114 | def to_dict(condition): 115 | fields = dict( 116 | argument='.'.join((condition.argument.__name__, condition.attribute)), 117 | negative=condition.negative, 118 | operator=condition.operator.name 119 | ) 120 | 121 | fields.update(condition.operator.variables) 122 | 123 | return fields 124 | 125 | 126 | class BaseConditionFormSet(BaseFormSet): 127 | 128 | @property 129 | def to_objects(self): 130 | return map(self.__make_condition, self.forms) 131 | 132 | def __make_condition(self, form): 133 | data = form.cleaned_data.copy() 134 | 135 | # Extract out the values from the POST data. These are all strings at 136 | # this point 137 | operator_str = data.pop('operator') 138 | negative_str = data.pop('negative') 139 | argument_str = data.pop('argument') 140 | 141 | # Operators in the registry are the types (classes), so extract that out 142 | # and we will construct it from the remaining data, which are the 143 | # arguments for the operator 144 | operator_type = operators[operator_str] 145 | 146 | # Arguments are a Class property, so just a simple fetch from the 147 | # arguments dict will retreive it 148 | argument = arguments[argument_str] 149 | 150 | # The remaining variables in the data are the arguments to the operator, 151 | # but they need to be cast by the argument to their right type 152 | caster = argument.variable.to_python 153 | data = dict((k, caster(v)) for k, v in data.items()) 154 | 155 | return Condition( 156 | argument=argument.owner, 157 | attribute=argument.name, 158 | operator=operator_type(**data), 159 | negative=bool(int(negative_str)) 160 | ) 161 | 162 | def value_at(self, index, field): 163 | if self.initial: 164 | return self.initial[index][field] 165 | elif index is not None: 166 | return self.data['form-%s-%s' % (index, field)] 167 | 168 | def add_fields(self, form, index): 169 | value = partial(self.value_at, index) 170 | 171 | for argument in operators.arguments.get(value('operator'), []): 172 | form.fields[argument] = forms.CharField(initial=value(argument)) 173 | 174 | super(BaseConditionFormSet, self).add_fields(form, index) 175 | 176 | 177 | ConditionFormSet = formset_factory( 178 | ConditionForm, 179 | formset=BaseConditionFormSet, 180 | extra=0 181 | ) 182 | 183 | 184 | class SwitchFormManager(object): 185 | 186 | def __init__(self, switch, condition_set): 187 | self.switch = switch 188 | self.conditions = condition_set 189 | 190 | @classmethod 191 | def from_post(cls, post_data): 192 | return cls(SwitchForm(post_data), ConditionFormSet(post_data)) 193 | 194 | def is_valid(self): 195 | return self.switch.is_valid() and self.conditions.is_valid() 196 | 197 | def save(self, gutter_manager): 198 | switch = self.switch.to_object 199 | try: 200 | switch.conditions = self.conditions.to_objects 201 | except AttributeError: 202 | switch.conditions = self.conditions 203 | 204 | gutter_manager.register(switch) 205 | 206 | def add_to_switch_list(self, switches): 207 | self.switch.conditions = self.conditions 208 | switches.insert(0, self.switch) 209 | 210 | def delete(self, gutter_manager): 211 | gutter_manager.unregister(self.switch.data['name']) 212 | -------------------------------------------------------------------------------- /gutter/django/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | gutter.helpers 3 | ~~~~~~~~~~~~~~~~ 4 | 5 | :copyright: (c) 2010 DISQUS. 6 | :license: Apache License 2.0, see LICENSE for more details. 7 | """ 8 | 9 | from django.core.serializers.json import DjangoJSONEncoder 10 | from django.utils import simplejson 11 | 12 | import datetime 13 | import uuid 14 | 15 | 16 | class BetterJSONEncoder(DjangoJSONEncoder): 17 | def default(self, obj): 18 | if isinstance(obj, uuid.UUID): 19 | return obj.hex 20 | elif isinstance(obj, datetime.datetime): 21 | return obj.strftime('%Y-%m-%dT%H:%M:%S.%fZ') 22 | elif isinstance(obj, (set, frozenset)): 23 | return list(obj) 24 | return super(BetterJSONEncoder, self).default(obj) 25 | 26 | 27 | def dumps(value, **kwargs): 28 | return simplejson.dumps(value, cls=BetterJSONEncoder, **kwargs) 29 | -------------------------------------------------------------------------------- /gutter/django/media/coffee/gutter.coffee: -------------------------------------------------------------------------------- 1 | swtch = 2 | disabled: '1' 3 | selective: '2' 4 | global: '3' 5 | 6 | update_conditions_visibility = (event) -> 7 | $conditions = $(this).parents('ul.switches li').find('section.conditions') 8 | 9 | switch $(this).val() 10 | when swtch.disabled, swtch.global then $conditions.hide() 11 | when swtch.selective then $conditions.show() 12 | 13 | remove_operator_arguments = (event) -> 14 | $(event.target).siblings('input[type=text]').remove() 15 | 16 | add_operator_arguments = (event) -> 17 | $operator = $(event.target) 18 | 19 | name_prefix = $operator.attr('name').split('-')[0..1].join('-') 20 | new_arguments = $operator.find('option:selected').data('arguments').split(',') 21 | 22 | $.each new_arguments, (index, argument) -> 23 | new_attrs = 24 | name: name_prefix + '-' + argument 25 | type: 'text' 26 | id: 'id_' + name_prefix + '-' + argument 27 | class: 'added' 28 | 29 | input = $('').attr(new_attrs) 30 | label = $('