Skip to content
This repository has been archived by the owner on Jan 31, 2018. It is now read-only.

Commit

Permalink
[bug 1086643] [bug 1086650] Redo infrastructure for product picker ve…
Browse files Browse the repository at this point in the history
…rsion

This redoes the infrastructure for having the generic form and the
product-picker version of it "live" simultaneously.

* nixed all the extra routing in favor of a waffle flag "feedbackdev"
* extracted product-picker specific unit tests and put them in their own file
* wrote a with_waffle class/function decorator that lets us more easily
  test waffley things
* added WAFFLE_OVERRIDE=True to settings--we always want to be able to
  explicitly set the waffle flag via the url
* added smoke tests for the dev form and product picker page
  • Loading branch information
willkg committed Oct 21, 2014
1 parent c9b83df commit cba9a2d
Show file tree
Hide file tree
Showing 20 changed files with 1,012 additions and 275 deletions.
116 changes: 116 additions & 0 deletions fjord/base/migrations/0003_create_dev_waffle_flag.py
@@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models

class Migration(DataMigration):

def forwards(self, orm):
"Write your forwards methods here."
# Note: Remember to use orm['appname.ModelName'] rather than "from appname.models..."
# Adds the waffle flag for a/b testing.
Flag = orm['waffle.flag']
flag = Flag(
name='feedbackdev',
everyone=False,
superusers=False,
staff=False,
authenticated=False,
rollout=False,
note='',
testing=False
)
flag.save()

def backwards(self, orm):
"Write your backwards methods here."
Flag = orm['waffle.flag']
try:
flag = Flag.objects.filter(name='feedbackdev')[0]
flag.delete()
except Flag.DoesNotExist:
pass

models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'base.profile': {
'Meta': {'object_name': 'Profile'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'waffle.flag': {
'Meta': {'object_name': 'Flag'},
'authenticated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
'everyone': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'languages': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'percent': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '3', 'decimal_places': '1', 'blank': 'True'}),
'rollout': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'superusers': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'testing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False', 'blank': 'True'})
},
u'waffle.sample': {
'Meta': {'object_name': 'Sample'},
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'percent': ('django.db.models.fields.DecimalField', [], {'max_digits': '4', 'decimal_places': '1'})
},
u'waffle.switch': {
'Meta': {'object_name': 'Switch'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'note': ('django.db.models.fields.TextField', [], {'blank': 'True'})
}
}

complete_apps = ['waffle', 'base']
symmetrical = True
75 changes: 72 additions & 3 deletions fjord/base/tests/__init__.py
Expand Up @@ -2,18 +2,18 @@
import os
from functools import wraps

from nose import SkipTest

from django.conf import settings
from django.contrib.auth.models import User
from django.core.cache import cache
from django.test import TestCase as OriginalTestCase
from django.test.client import Client
from django.test.utils import override_settings

import factory
import waffle
from django_browserid.tests import mock_browserid
from nose import SkipTest

import factory
# reverse is here for convenience so other test modules import it from
# here rather than importing it from urlresolvers
from fjord.base.urlresolvers import reverse # noqa
Expand Down Expand Up @@ -65,6 +65,75 @@ def _skipping_fun(*args, **kwargs):
return skipping_cls_or_fun


def with_waffle(flagname, flagvalue=True):
"""Decorator that enables a given flag
You can wrap a test class with this decorator and all the tests in
the class will run with the waffle flag enabled/disabled.
You can wrap a single test function with this decorator and that
test will run with the waffle flag enabled/disabled.
Usage::
@with_waffle('some_flag', True)
class TestClass(TestCase):
...
@with_waffle('some_flag', False)
def test_my_view():
...
"""

def with_waffle_cls_or_fun(cls_or_func):
"""Class or function decorator for enabling waffle flags"""

def give_me_waffles(func):
"""Function decorator for enabling the waffle flag"""
@wraps(func)
def _give_me_waffles(*args, **kwargs):
origvalue = None
try:
flag = waffle.Flag.objects.filter(name=flagname)[0]
origvalue = flag.everyone
flag.everyone = flagvalue
except waffle.Flag.DoesNotExist:
flag = waffle.Flag(name=flagname, everyone=True)
flag.save()

try:
return func(*args, **kwargs)
except Exception:
# FIXME: This breaks if saving the flag also
# raises an exception, but that really shouldn't
# happen in our test suite and if it does, we've
# probably got other more serious issues to deal
# with.
if origvalue is not None:
flag.everyone = origvalue
flag.save()
raise
return _give_me_waffles

if inspect.isclass(cls_or_func):
# If cls_or_func is a class, then we wrap all the callable
# methods that start with 'test'.
for attr in cls_or_func.__dict__.keys():
if (attr.startswith('test')
and callable(getattr(cls_or_func, attr))):

setattr(cls_or_func, attr,
give_me_waffles(getattr(cls_or_func, attr)))
return cls_or_func
else:
# If cls_or_func is a function, then we return the
# skipping_fun
return give_me_waffles(cls_or_func)

return with_waffle_cls_or_fun


class LocalizingClient(Client):
"""Client which rewrites urls to include locales and adds a user agent.
Expand Down
133 changes: 23 additions & 110 deletions fjord/feedback/templates/feedback/generic_feedback.html
@@ -1,117 +1,30 @@
{% extends "feedback/base.html" %}

{% block page_title %}{{ _('Submit Your Feedback') }}{% endblock %}

{% set extra_body_attrs = {'data-form-name': 'generic'} %}

{% block body %}
<form id="responseform" action="" method="post">
<x-deck transition-type="slide-left">
<x-card id="intro">
<x-appbar>
<header>{{ _('Submit Your Feedback') }}</header>
</x-appbar>

<section>
<div id="sentiment">
<p>{{ _('Your feedback helps us improve Firefox.') }}</p>
<div id="buttons">
<button class="happy">{{ _('Firefox made me happy') }}</button>
<button class="sad">{{ _('Firefox made me sad') }}</button>
</div>
</div>

<aside>
<div>
{% trans support_url='http://support.mozilla.org/' %}
If you need help or have a problem
with Firefox, please visit <a href="{{ support_url }}">Firefox Support</a>.
{% endtrans %}
</div>
</aside>
</section>
</x-card>

<x-card id="moreinfo">
<x-appbar>
<button class="back"></button>
<header>{{ _('Details') }}</header>
</x-appbar>

<section>
<aside>
<div>
{% trans %}
The content of your feedback will be public, so please be
sure not to include any personal information.
{% endtrans %}
</div>
</aside>

<p>
<label class="happy" for="description">
{{ _('Please describe what you liked.') }}
</label>
<label class="sad" for="description">
{{ _('Please describe your problem below.') }}
{{ _('Please be as specific as you can.') }}
</label>
</p>

<div id="description-counter"></div>
<textarea data-max-length="10000" id="description" name="description" cols="40" rows="4"></textarea>

<div>
<label for="id_url">
{{ _('If your feedback is related to a website, you can include it here.') }}
</label>
<p>{{ form.url }}</p>
{{ form.url.errors }}
</div>

<button id="description-next-btn" class="next btn submit">{{ _('Next') }}</button>
</section>
</x-card>

<x-card id="email">
<x-appbar>
<button class="back"></button>
<header>{{ _('Can we contact you?') }}</header>
</x-appbar>

<section>
<label class="email-ok">
<p>
<input id="email-ok" type="checkbox" name="email_ok"/>
{{ _('Check here to let us contact you to follow up on your feedback.') }}
</p>
</label>
{% block site_css %}
{% if waffle.flag('feedbackdev') %}
{{ css('generic_feedback_dev') }}
{% else %}
{{ css('generic_feedback') }}
{% endif %}
{% endblock %}

<div id="email-details">
<label for="id_email">
{{ _('Email address (optional):') }}
</label>
<p>{{ form.email }}</p>
{{ form.email.errors }}
{% block site_js %}
<script src="{{ settings.STATIC_URL }}js/lib/brick-1.0.0.byob.min.js"></script>
{% if waffle.flag('feedbackdev') %}
{{ js('generic_feedback_dev') }}
{% else %}
{{ js('generic_feedback') }}
{% endif %}
{% endblock %}

<aside>
<div>
{% trans %}
While your feedback will be publicly visible, email addresses are
kept private. We understand your privacy is important.
{% endtrans %}
</div>
</aside>
</div>
{% block page_title %}{% if waffle.flag('feedbackdev') %}[DEV] {% endif %}{{ _('Submit Your Feedback') }}{% endblock %}

<button id="form-submit-btn" class="complete btn submit">{{ _('Send Feedback') }}</button>
</section>
</x-card>
{% set extra_body_attrs = {'data-form-name': 'generic'} %}

</x-deck>
{% for hidden in form.hidden_fields() %}
{{ hidden }}
{% endfor %}
{{ csrf() }}
</form>
{% block body %}
{% if waffle.flag('feedbackdev') %}
{% include 'feedback/generic_feedback_form_dev.html' %}
{% else %}
{% include 'feedback/generic_feedback_form.html' %}
{% endif %}
{% endblock %}

0 comments on commit cba9a2d

Please sign in to comment.