├── .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'')
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 = $('