From 8fc2d2e941ca3533c7a225a2c2cab313342898a4 Mon Sep 17 00:00:00 2001 From: Michael Scheibe Date: Fri, 27 May 2011 17:58:10 -0700 Subject: [PATCH] Add session specific controls to dashboard so that users who have permission to view the admin tool (staff) can more easily enable/disable a feature for just their session. This is useful for testing/debugging workflow. The new options are: - Session Enable - Session Disable - Session Bypass (The default, which falls back to the real status.) Implementation wise, this works by adding a session cookie per switch. Restarting the browser will delete the cookie, and normal users will not have any extra cookies. --- gargoyle/media/css/gargoyle.css | 8 +++- gargoyle/media/js/gargoyle.js | 28 ++++++++++- gargoyle/models.py | 47 ++++++++++++++----- gargoyle/nexus_modules.py | 46 ++++++++++++++++-- gargoyle/templates/gargoyle/index.html | 45 +++++++++++++++++- gargoyle/tests/tests.py | 64 +++++++++++++++++++------- 6 files changed, 201 insertions(+), 37 deletions(-) diff --git a/gargoyle/media/css/gargoyle.css b/gargoyle/media/css/gargoyle.css index cd1d331d..d17518bc 100644 --- a/gargoyle/media/css/gargoyle.css +++ b/gargoyle/media/css/gargoyle.css @@ -133,11 +133,14 @@ margin-bottom: 2px; } -#container table.switches td.status { +#container table.switches td.status, #container table.switches td.sessionStatus { width: 200px; text-align: center; } -#container table.switches td.status p { +#container table.switches td.sessionStatus { + width: 330px; +} +#container table.switches td.status p, #container table.switches td.sessionStatus p { color: #999; margin-top: 15px; font-size: 85%; @@ -145,6 +148,7 @@ margin-bottom: 0; } + #container table.switches td.actions { text-align: right; width: 70px; diff --git a/gargoyle/media/js/gargoyle.js b/gargoyle/media/js/gargoyle.js index 74bb6419..80e85285 100644 --- a/gargoyle/media/js/gargoyle.js +++ b/gargoyle/media/js/gargoyle.js @@ -98,7 +98,7 @@ $(document).ready(function () { function (swtch) { if (swtch.status == status) { - row.find(".toggled").removeClass("toggled"); + row.find(".status .toggled").removeClass("toggled"); el.addClass("toggled"); if (!swtch.conditions && swtch.status == 2) { swtch.status = 3; @@ -108,6 +108,32 @@ $(document).ready(function () { }); }); + $(".switches td.sessionStatus button").live("click", function () { + var row = $(this).parents("tr:first"); + var el = $(this); + var session_status = el.attr("data-session-status"); + var labels = {} + labels[GARGOYLE.SESSION_BYPASS] = "(No manual override)" + labels[GARGOYLE.SESSION_DISABLED] = "(Manually disabled for my session)" + labels[GARGOYLE.SESSION_ENABLED] = "(Manually enabled for my session)" + + api(GARGOYLE.updateSessionStatus, + { + key: row.attr("data-switch-key"), + session_status: session_status + }, + + function (data) { + if (data.session_status == session_status) { + document.cookie = data.cookie_key + '=' + data.cookie_value + ';path=/' + row.find(".sessionStatus .toggled").removeClass("toggled"); + el.addClass("toggled"); + row.find('.sessionStatus p').text(labels[data.session_status]); + } + }); + }); + + $("p.addCondition a").live("click", function (ev) { ev.preventDefault(); var form = $(this).parents("td:first").find("div.conditionsForm:first"); diff --git a/gargoyle/models.py b/gargoyle/models.py index c96d8aa6..dd34edb4 100644 --- a/gargoyle/models.py +++ b/gargoyle/models.py @@ -8,6 +8,8 @@ from jsonfield import JSONField from modeldict import ModelDict +from nexus import site + DISABLED = 1 SELECTIVE = 2 GLOBAL = 3 @@ -15,6 +17,16 @@ INCLUDE = 'i' EXCLUDE = 'e' +SESSION_BYPASS = 'bypass' +SESSION_DISABLED = 'disabled' +SESSION_ENABLED = 'enabled' + +SESSION_STATUS_CHOICES = ( + (SESSION_BYPASS, 'Session Bypass'), + (SESSION_ENABLED, 'Session Enabled'), + (SESSION_DISABLED, 'Session Disabled'), +) + class Switch(models.Model): """ Stores information on all switches. Generally handled through an instance of ``ModelDict``, @@ -28,13 +40,13 @@ class Switch(models.Model): >>> } >>> } """ - + STATUS_CHOICES = ( (DISABLED, 'Disabled'), (SELECTIVE, 'Selective'), (GLOBAL, 'Global'), ) - + key = models.CharField(max_length=32, primary_key=True) value = JSONField(default="{}") label = models.CharField(max_length=32, null=True) @@ -143,7 +155,7 @@ def remove_condition(self, manager, condition_set, field_name, condition, commit if namespace not in self.value: return - + if field_name not in self.value[namespace]: return @@ -176,14 +188,14 @@ def clear_conditions(self, manager, condition_set, field_name=None, commit=True) if namespace not in self.value: return - + if not field_name: del self.value[namespace] elif field_name not in self.value[namespace]: return else: del self.value[namespace][field_name] - + if commit: self.save() @@ -265,12 +277,28 @@ def is_active(self, key, *instances): >>> gargoyle.is_active('my_feature', request) #doctest: +SKIP """ - + try: switch = self[key] except KeyError: return False + if instances: + # HACK: support request.user by swapping in User instance + instances = list(instances) + for v in instances: + if isinstance(v, HttpRequest) and hasattr(v, 'user'): + instances.append(v.user) + if site.has_permission(v): + try: + session_status = v.COOKIES.get('switch_%s' % key) + if session_status == SESSION_DISABLED: + return False + elif session_status == SESSION_ENABLED: + return True + except ValueError: + pass + if switch.status == GLOBAL: return True elif switch.status == DISABLED: @@ -281,11 +309,6 @@ def is_active(self, key, *instances): return True if instances: - # HACK: support request.user by swapping in User instance - instances = list(instances) - for v in instances: - if isinstance(v, HttpRequest) and hasattr(v, 'user'): - instances.append(v.user) for instance in instances: # check each switch to see if it can execute @@ -295,7 +318,7 @@ def is_active(self, key, *instances): return True return False - + def register(self, condition_set): """ Registers a condition set with the manager. diff --git a/gargoyle/nexus_modules.py b/gargoyle/nexus_modules.py index 94578fef..b48df52c 100644 --- a/gargoyle/nexus_modules.py +++ b/gargoyle/nexus_modules.py @@ -8,7 +8,14 @@ from django.utils import simplejson from gargoyle import gargoyle, autodiscover -from gargoyle.models import Switch, DISABLED +from gargoyle.models import ( + Switch, + DISABLED, + SESSION_BYPASS, + SESSION_ENABLED, + SESSION_DISABLED, +) + from gargoyle.conditions import ValidationError GARGOYLE_ROOT = os.path.dirname(__file__) @@ -70,6 +77,7 @@ def get_urls(self): url(r'^update/$', self.as_view(self.update), name='update'), url(r'^delete/$', self.as_view(self.delete), name='delete'), url(r'^status/$', self.as_view(self.status), name='status'), + url(r'^status/session/$', self.as_view(self.session_status), name='session-status'), url(r'^conditions/add/$', self.as_view(self.add_condition), name='add-condition'), url(r'^conditions/remove/$', self.as_view(self.remove_condition), name='remove-condition'), url(r'^$', self.as_view(self.index), name='index'), @@ -89,11 +97,41 @@ def render_on_dashboard(self, request): def index(self, request): switches = list(Switch.objects.all().order_by("date_created")) + switches = [s.to_dict(gargoyle) for s in switches] + for s in switches: + session_status = request.COOKIES.get('switch_%s' % (s['key'])) + if session_status not in (SESSION_ENABLED, SESSION_DISABLED): + session_status = SESSION_BYPASS + + s['session_status'] = session_status - return self.render_to_response("gargoyle/index.html", { - "switches": [s.to_dict(gargoyle) for s in switches], + context = { + "switches": switches, "all_conditions": list(gargoyle.get_all_conditions()), - }, request) + "SESSION_ENABLED": SESSION_ENABLED, + "SESSION_DISABLED": SESSION_DISABLED, + "SESSION_BYPASS": SESSION_BYPASS, + } + + return self.render_to_response("gargoyle/index.html", context, request) + + def session_status(self, request): + switch = Switch.objects.get(key=request.POST.get("key")) + + session_status = request.POST.get("session_status") + if session_status not in ( + SESSION_BYPASS, + SESSION_DISABLED, + SESSION_ENABLED, + ): + raise GargoyleException("Not a valid Session Status") + + return { + 'cookie_key': 'switch_%s' % (request.POST.get("key")), + 'cookie_value': session_status, + 'session_status': session_status, + } + session_status = json(session_status) def add(self, request): key = request.POST.get("key") diff --git a/gargoyle/templates/gargoyle/index.html b/gargoyle/templates/gargoyle/index.html index 9f9c2501..fc4a4fec 100644 --- a/gargoyle/templates/gargoyle/index.html +++ b/gargoyle/templates/gargoyle/index.html @@ -11,6 +11,7 @@ updateSwitch: "{% url gargoyle:update %}", deleteSwitch: "{% url gargoyle:delete %}", updateStatus: "{% url gargoyle:status %}", + updateSessionStatus: "{% url gargoyle:session-status %}", addCondition: "{% url gargoyle:add-condition %}", delCondition: "{% url gargoyle:remove-condition %}", @@ -18,7 +19,10 @@ facebox: { loadingImage: "{% url nexus:media 'gargoyle' 'img/facebox/loading.gif' %}", closeImage: "{% url nexus:media 'gargoyle' 'img/facebox/closelabel.png' %}" - } + }, + SESSION_BYPASS: "{{SESSION_BYPASS}}", + SESSION_ENABLED: "{{SESSION_ENABLED}}", + SESSION_DISABLED: "{{SESSION_DISABLED}}" }; @@ -69,6 +73,28 @@

{{ switch.label }} ({{ switch.key }})

+ + + + + + +

+ {% if switch.session_status == SESSION_ENABLED %} + (Manually enabled for my session) + {% else %}{% if switch.session_status == SESSION_DISABLED %} + (Manually disabled for my session) + {% else %} + (No manual override) + {% endif %}{% endif %} +

+ + + + + +

+ (No manual override) +

+