Skip to content

Commit

Permalink
Bugfixes and refactoring.
Browse files Browse the repository at this point in the history
  • Loading branch information
Travis Dart committed May 31, 2020
1 parent 2599b6f commit f7efbfd
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 143 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ of valid and invalid values for each field:

class Book(models.Model):
title = models.CharField(max_length=100, blank=False, null=False)
subtitle = models.CharField(max_length=100)
subtitle = models.CharField(max_length=100, blank=True, default="")


class BookForm(ModelForm):
Expand All @@ -29,11 +29,11 @@ of valid and invalid values for each field:
form = BookForm
title = FormElement(
good = ["Moby Dick"],
bad = [None, '', 'A'*101],
bad = [None, "", "A"*101],
)
subtitle = FormElement(
good = [None, "", "or The Whale"],
bad = ["A"*101]
good = ["", "or The Whale"],
bad = [None, "A"*101]
)


Expand All @@ -51,11 +51,11 @@ each field's possible values. Namely, the form will be tested with these values:

| title | subtitle | result |
|-----------|--------------|---------|
| Moby Dick | None | Valid |
| Moby Dick | "" | Valid |
| Moby Dick | None | Invalid |
| None | None | Invalid |
| "" | None | Invalid |
| AA[...]AA | None | Invalid |
| Moby Dick | "" | Valid |
| None | "" | Invalid |
| "" | "" | Invalid |
| AA[...]AA | "" | Invalid |
Expand Down
56 changes: 46 additions & 10 deletions formstorm/FormElement.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,55 @@ def __init__(self,
self.only_if = only_if
self.not_if = not_if

def build_iterator(self, ref_model, is_e2e):
for i, g in enumerate(self.good):
if type(g) is Q:
ref_object = ref_model.objects.get(g)
if is_e2e: # If we're doing an e2e test, reference by name.
self.good[i] = text_type(ref_object)
else:
self.good[i] = ref_object.pk

def build_iterator(self, form, field_name, is_e2e):
"""
Suppose in a Book model, Book.genre is a foreign key to a Genre model,
and in the Book test we have:
genre = FormElement(
good=[Q(name="Mystery")],
)
If good=[Q(name="Mystery")] then replace good with the pk of the
Mystery object. Essentially (although this is not the algorithm used)
the object is replaced with:
good = [ Genre.objects.get(name="Mystery").pk ]
"""
def _get_pk_for_q(q_object):
# 1st: Find the model that the field points to.
form_field = form._meta.model._meta.get_field(field_name)
try: # Python 3
ref_model = form_field.remote_field.model
except AttributeError:
ref_model = form_field.rel.to

# 2nd: Get the object that the Q object points to.
ref_object = ref_model.objects.get(q_object)

# 3rd: Get the identifier for that object.
if is_e2e: # If we're doing an e2e test, reference by name.
return text_type(ref_object)
else:
return ref_object.pk

def _replace_all_q(value_list):
for i, g in enumerate(value_list):
if type(g) is Q:
value_list[i] = _get_pk_for_q(g)
elif type(g) in [list, tuple]:
value_list[i] = [
_get_pk_for_q(j)
if type(j) is Q else j
for j in g
]
return value_list

self.good = _replace_all_q(self.good)
self.bad = _replace_all_q(self.bad)
self.iterator = chain(
[(x, True) for x in self.good],
[(x, False) for x in self.bad],
# self.values
self.values
)

return self.iterator
26 changes: 10 additions & 16 deletions formstorm/FormTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,11 @@ def _build_elements(self):
for e in dir(self):
# Filter out this class's FormElement properties
if type(getattr(self, e)) is FormElement:
# If this field is a fk/m2m, get the model that it points to.
form_field = self.form._meta.model._meta.get_field(e)
try: # Python 3
ref_model = form_field.remote_field.model
except AttributeError:
try:
ref_model = form_field.rel.to
except AttributeError:
ref_model = None

self.elements[e] = getattr(self, e).build_iterator(
is_e2e=self.is_e2e,
ref_model=ref_model
form=self.form,
field_name=e
)
getattr(self, e)

def __init__(self):
self._is_modelform = ModelForm in self.form.mro()
Expand All @@ -43,18 +33,22 @@ def __init__(self):
self._iterator = dict_combo(self.elements)

def _run(self, is_uniqueness_test=False):
# i is a dictionary whose elements are tuples
# in the form (value, is_good)
# i is a dictionary containing tuples in the form (value, is_good)
for i in self._iterator:
# if any field is invalid, the form is invalid.
form_is_good = all([x[1][1] for x in i.items()])
form_values = {k: v[0] for k, v in i.items()}
form_values = {k: v[0] for k, v in i.items() if v[0] is not None}

if self._is_modelform and not is_uniqueness_test:
sid = transaction.savepoint()

self.submit_form(form_values)
assert self.is_good() == form_is_good

if self.is_good() != form_is_good:
# print("form_values", form_values)
# print("errors", self.bound_form.errors)
raise AssertionError

if is_uniqueness_test and form_is_good:
self.submit_form(form_values)
assert not self.is_good()
Expand Down
50 changes: 23 additions & 27 deletions formstorm/iterhelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ def every_combo(items):
list(every_combo([1, 2, 3]))
[(1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)]
"""
return itertools.chain(*[
itertools.combinations(items, i)
for i in range(1, len(items) + 1)
])
x = [itertools.combinations(items, i) for i in range(1, len(items) + 1)]
y = list(itertools.chain(*x))
return y


def dict_combo(params):
Expand All @@ -22,36 +21,33 @@ def dict_combo(params):
"c": [True, False, None],
}
for x in dict_combo(params):
print x
print(x)
Returns:
{'a': 'A', 'c': True, 'b': 0}
{'a': 'A', 'c': True, 'b': 1}
{'a': 'A', 'c': False, 'b': 0}
{'a': 'A', 'c': False, 'b': 1}
{'a': 'A', 'c': None, 'b': 0}
{'a': 'A', 'c': None, 'b': 1}
{'a': 'B', 'c': True, 'b': 0}
{'a': 'B', 'c': True, 'b': 1}
{'a': 'B', 'c': False, 'b': 0}
{'a': 'B', 'c': False, 'b': 1}
{'a': 'B', 'c': None, 'b': 0}
{'a': 'B', 'c': None, 'b': 1}
{'a': 'C', 'c': True, 'b': 0}
{'a': 'C', 'c': True, 'b': 1}
{'a': 'C', 'c': False, 'b': 0}
{'a': 'C', 'c': False, 'b': 1}
{'a': 'C', 'c': None, 'b': 0}
{'a': 'C', 'c': None, 'b': 1}
{'a': 'A', 'b': 0, 'c': True}
{'a': 'A', 'b': 0, 'c': False}
{'a': 'A', 'b': 0, 'c': None}
{'a': 'A', 'b': 1, 'c': True}
{'a': 'A', 'b': 1, 'c': False}
{'a': 'A', 'b': 1, 'c': None}
{'a': 'B', 'b': 0, 'c': True}
{'a': 'B', 'b': 0, 'c': False}
{'a': 'B', 'b': 0, 'c': None}
{'a': 'B', 'b': 1, 'c': True}
{'a': 'B', 'b': 1, 'c': False}
{'a': 'B', 'b': 1, 'c': None}
{'a': 'C', 'b': 0, 'c': True}
{'a': 'C', 'b': 0, 'c': False}
{'a': 'C', 'b': 0, 'c': None}
{'a': 'C', 'b': 1, 'c': True}
{'a': 'C', 'b': 1, 'c': False}
{'a': 'C', 'b': 1, 'c': None}
"""
# We don't want the key-->value paring to get mismatched.
# Convert a dictionary to (key, value) tuples,
# Then convert to a list of keys and a list of values
# The index of the key will be the index of the value.
keys, values = zip(*[
(key, value)
for key, value in params.items()
])
keys, values = zip(*[(key, value) for key, value in params.items()])
# Take the cartesian product of all the sets.
# This will return an iterator with one item from each set,
# We take this an pack it back into a dictionary.
Expand Down
39 changes: 11 additions & 28 deletions tests/fstest/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@
import os
import sys


# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Manually add the formstorm folder to the Python path.
sys.path.append(os.path.dirname(BASE_DIR))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/

Expand All @@ -32,18 +30,12 @@

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',

'fstestapp'
'django.contrib.admin', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.staticfiles', 'fstestapp'
]

MIDDLEWARE = [
Expand Down Expand Up @@ -76,7 +68,6 @@

WSGI_APPLICATION = 'fstest.wsgi.application'


# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases

Expand All @@ -87,35 +78,28 @@
}
}


# Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
{
'NAME': (
'django.contrib.auth.password_validation.'
'UserAttributeSimilarityValidator'
),
'NAME': ('django.contrib.auth.password_validation.'
'UserAttributeSimilarityValidator'),
},
{
'NAME': (
'django.contrib.auth.password_validation.MinimumLengthValidator'
),
'NAME':
('django.contrib.auth.password_validation.MinimumLengthValidator'),
},
{
'NAME': (
'django.contrib.auth.password_validation.CommonPasswordValidator'
),
'NAME':
('django.contrib.auth.password_validation.CommonPasswordValidator'),
},
{
'NAME': (
'django.contrib.auth.password_validation.NumericPasswordValidator'
),
'NAME':
('django.contrib.auth.password_validation.NumericPasswordValidator'),
},
]


# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/

Expand All @@ -129,7 +113,6 @@

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/

Expand Down
8 changes: 0 additions & 8 deletions tests/fstestapp/apps.py

This file was deleted.

46 changes: 13 additions & 33 deletions tests/fstestapp/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-08-20 03:46
from __future__ import unicode_literals
# Generated by Django 3.0.5 on 2020-04-19 04:51

from django.db import migrations, models
import django.db.models.deletion
Expand All @@ -17,45 +15,27 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Author',
fields=[
('id', models.AutoField(auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID')),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='Book',
name='Genre',
fields=[
('id', models.AutoField(auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID')),
('title', models.CharField(max_length=100, unique=True)),
('subtitle', models.CharField(blank=True,
max_length=100,
null=True)),
('is_fiction', models.BooleanField(default=False)),
('pages', models.IntegerField(default=False)),
('author', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='fstestapp.Author')
),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='Genre',
name='Book',
fields=[
('id', models.AutoField(auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID')),
('name', models.CharField(max_length=100)),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100, unique=True)),
('subtitle', models.CharField(blank=True, default='', max_length=100)),
('is_fiction', models.BooleanField(default=False)),
('pages', models.IntegerField(default=False)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fstestapp.Author')),
('genre', models.ManyToManyField(to='fstestapp.Genre')),
],
),
migrations.AddField(
model_name='book',
name='genre',
field=models.ManyToManyField(to='fstestapp.Genre'),
),
]
Loading

0 comments on commit f7efbfd

Please sign in to comment.