Add integration tests (#20)

* Adding integration tests with inlines

* Adding more tests

* FIx make file

* Setup CI build matrix to work with integration tests

* Try again

* Fix workflow synctax

* Clean up workflow

* Format and some lint stuff

* Try codecov

* yml

* More Testing

* Minor lint things

* Update

* Try again for codecov

* Updates

* Try?

* Ignore quotes

* Exclude test project

* try this?

* checkout required

* Rename

* clean up configs

* Fix

* Allow to fail

* ignores

* FInish the integration tests for cache

* Fix workflow yml

* fix

* Try up again

* TRy again

* Fix

* Fix

* Fix tests

Co-authored-by: Thu Trang Pham <thu@joinmodernhealth.com>
main
Thu Trang Pham 2021-03-05 19:54:01 -08:00 committed by GitHub
parent 4f50c63f7b
commit ad7409b567
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 677 additions and 185 deletions

View File

@ -1,4 +0,0 @@
[run]
relative_files = True
omit = admin_confirm/tests/*
branch = True

2
.env 100644
View File

@ -0,0 +1,2 @@
PYTHON_VERSION=3.8
DJANGO_VERSION=3.1.7

View File

@ -14,47 +14,41 @@ on:
- created
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: wemake-python-styleguide
uses: wemake-services/wemake-python-styleguide@0.15.2
with:
path: admin_confirm
reporter: 'github-pr-review'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
django-version: [2.2, 3.0]
env:
DJANGO_VERSION: ${{ matrix.django-version }}
PYTHON_VERSION: ${{ matrix.python-version }}
COMPOSE_INTERACTIVE_NO_CLI: 1
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install Django ${{ matrix.django-version }}
- name: Build Docker for Python 3.6
if: ${{ matrix.python-version == 3.6 }}
run: |
pip install django==${{ matrix.django-version }}
- name: Install dependencies
export SELENIUM_VERSION=3.141.0
docker-compose build
- name: Build Docker for other Python versions
if: ${{ matrix.python-version != 3.6 }}
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Unit Test
run: |
make test
- name: Coveralls
uses: AndreMiras/coveralls-python-action@develop
with:
parallel: true
flag-name: Unit Test
integration-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build Docker
run: docker-compose build
export SELENIUM_VERSION=4.0.0a7
docker-compose build
- name: Start Docker
run: docker-compose up -d
- name: Integration Test
@ -63,10 +57,9 @@ jobs:
uses: AndreMiras/coveralls-python-action@develop
with:
parallel: true
flag-name: Integration Test
coveralls:
needs: [test, integration-test]
needs: [test]
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished

View File

@ -1,7 +1,12 @@
FROM python:3
ARG PYTHON_VERSION=3.8
FROM python:${PYTHON_VERSION}
ENV PYTHONUNBUFFERED=1
ENV USE_DOCKER=true
WORKDIR /code
COPY requirements.txt /code/
RUN pip install -r requirements.txt
COPY . /code/
ARG DJANGO_VERSION="3.1.7"
RUN pip install django==${DJANGO_VERSION}
RUN pip install -r requirements.txt
RUN pip install -e .
ARG SELENIUM_VERSION="4.0.0a7"
RUN pip install selenium~=${SELENIUM_VERSION}

View File

@ -153,6 +153,13 @@ pyenv vituralenv 3.8.0 django-admin-confirm-3.8.0
Now your terminal should have `(django-admin-confirm-3.8.0)` prefix, because `.python-version` should have auto switch your virtual env
Install requirements
```
pip install -r requirements.txt
pip install -e .
```
Run **migrations** and create a superuser and run the server
```
@ -237,7 +244,6 @@ pip uninstall django_admin_confirm
make install-testpypi VERSION=<VERSION>
```
Update version in `requirements.txt`
Add test locally
```

View File

@ -11,7 +11,6 @@ from django.forms import ModelForm
from admin_confirm.utils import get_admin_change_url, snake_to_title_case
from django.core.cache import cache
from django.views.decorators.cache import cache_control
from django.forms.formsets import all_valid
from admin_confirm.constants import (
CACHE_TIMEOUT,
CONFIRMATION_RECEIVED,
@ -139,7 +138,7 @@ class AdminConfirmMixin:
return [initial_value, new_value]
if initial_value:
if new_value == False:
if new_value is False:
# Clear has been selected
return [initial_value.name, None]
elif new_value:
@ -249,7 +248,7 @@ class AdminConfirmMixin:
if CONFIRM_CHANGE in modified_post:
del modified_post[CONFIRM_CHANGE]
if object_id and not SAVE_AS_NEW in request.POST:
if object_id and SAVE_AS_NEW not in request.POST:
# Update the obj with the new uploaded files
# then pass rest of changes to Django
obj = self.model.objects.filter(id=object_id).first()
@ -287,6 +286,20 @@ class AdminConfirmMixin:
cache.delete_many(CACHE_KEYS.values())
return super()._changeform_view(request, object_id, form_url, extra_context)
def _get_cleared_fields(self, request):
"""
Checks for any ImageField or FileField which have been cleared by user.
Because the form that is generated by Django for the model, would not have the
`<field>-clear` inputs in them, they have to be injected into the hidden form
on the confirmation page.
"""
return [
input_name.split("-clear")[0]
for input_name in request.POST.keys()
if input_name.endswith("-clear")
]
def _change_confirmation_view(self, request, object_id, form_url, extra_context):
# This code is taken from super()._changeform_view
# https://github.com/django/django/blob/master/django/contrib/admin/options.py#L1575-L1592
@ -348,10 +361,14 @@ class AdminConfirmMixin:
save_action = key
break
cleared_fields = []
if form.is_multipart():
cache.set(CACHE_KEYS["post"], request.POST, timeout=CACHE_TIMEOUT)
cache.set(CACHE_KEYS["object"], new_object, timeout=CACHE_TIMEOUT)
# Handle when files are cleared - since the `form` object would not hold that info
cleared_fields = self._get_cleared_fields(request)
title_action = _("adding") if add_or_new else _("changing")
context = {
**self.admin_site.each_context(request),
@ -368,6 +385,7 @@ class AdminConfirmMixin:
"save_as_new": SAVE_AS_NEW in request.POST,
"submit_name": save_action,
"form": form,
"cleared_fields": cleared_fields,
"formsets": formsets,
**(extra_context or {}),
}

View File

@ -40,8 +40,11 @@
{% include "admin/change_data.html" %}
<form {% if form.is_multipart %}enctype="multipart/form-data"{% endif %} method="post" {% if add %}action="{% url opts|admin_urlname:'add'%}" {% else %}action="{% url opts|admin_urlname:'change' object_id|admin_urlquote %}"{% endif %}>{% csrf_token %}
<div class="hidden">
<div class="hidden" id="hidden-form">
{{form.as_p}}
{% for cleared_field in cleared_fields %}
<input type="checkbox" name="{{ cleared_field }}-clear" checked>
{% endfor %}
{% for formset in formsets %}
{{ formset.as_p }}
{% endfor %}

View File

@ -1,5 +1,4 @@
from django import template
from django.db.models.query import QuerySet
from django.utils.html import escape
from django.utils.safestring import mark_safe
@ -16,5 +15,5 @@ def format_change_data_field_value(field_value):
output += "<li>" + escape(value) + "</li>"
output += "</ul>"
return mark_safe(output)
except:
except Exception:
return field_value

View File

@ -8,6 +8,12 @@ You seem concerned about the stability and reliability of this package. You're p
So if you want to include this package in your production codebase, be aware that AdminConfirmMixin works best with simple unmodified ModelAdmins.
# Probable Issues
These are some areas which might/probably have issues that are not currently tested. Use at your own risk!
- [ ] Saving file/image changes on inlines when confirming change on parent model
## Save Options
- [x] Save
@ -71,10 +77,10 @@ Confirmation on inline changes is not a current feature of this project.
Confirmation on add/change of ModelAdmin that includes inlines needs to be tested. Use AdminConfirmMixin with ModelAdmin containing inlines at your own risk.
- [ ] .inlines
- [ ] .get_inline_instances()
- [ ] .get_inlines() (New in Django 3.0)
- [ ] .get_formsets_with_inlines()
- [x] .inlines
- [x] .get_inline_instances()
- [x] .get_inlines() (New in Django 3.0)
- [ ] .get_formsets_with_inlines() ???
#### Options for inlines

View File

@ -1,6 +1,12 @@
import socket
from django.core.cache import cache
from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User
from django.test import LiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
class AdminConfirmTestCase(TestCase):
"""
@ -62,16 +68,10 @@ class AdminConfirmTestCase(TestCase):
self.assertIn("apple", rendered_content)
import socket
from django.test import LiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
class AdminConfirmIntegrationTestCase(LiveServerTestCase):
@classmethod
def setUpClass(cls):
cls.host = socket.gethostbyname(socket.gethostname())
cls.selenium = webdriver.Remote(
command_executor="http://selenium:4444/wd/hub",
@ -79,6 +79,25 @@ class AdminConfirmIntegrationTestCase(LiveServerTestCase):
)
super().setUpClass()
def setUp(self):
self.superuser = User.objects.create_superuser(
username="super", email="super@email.org", password="pass"
)
self.client.force_login(self.superuser)
cookie = self.client.cookies["sessionid"]
self.selenium.get(
self.live_server_url + "/admin/"
) # selenium will set cookie domain based on current page domain
self.selenium.add_cookie(
{"name": "sessionid", "value": cookie.value, "secure": False, "path": "/"}
)
return super().setUp()
def tearDown(self):
cache.clear()
return super().tearDown()
@classmethod
def tearDownClass(cls):
cls.selenium.quit()

View File

@ -3,5 +3,6 @@ from admin_confirm.tests.helpers import AdminConfirmIntegrationTestCase
class SmokeTest(AdminConfirmIntegrationTestCase):
def test_load_admin(self):
self.selenium.get(self.live_server_url+'/admin/')
self.assertIn('Django', self.selenium.title)
self.selenium.get(self.live_server_url + "/admin/")
self.assertIn("Django", self.selenium.title)
self.assertIn("Market", self.selenium.page_source)

View File

@ -0,0 +1,213 @@
"""
Tests confirmation of add/change
on ModelAdmin that utilize caches
"""
import os
import pytest
import pkg_resources
from importlib import reload
from tests.factories import ShopFactory
from tests.market.models import GeneralManager, Item, ShoppingMall, Town
from admin_confirm.tests.helpers import AdminConfirmIntegrationTestCase
from tests.market.admin import shoppingmall_admin
from admin_confirm.constants import CONFIRM_CHANGE
from selenium.webdriver.support.ui import Select
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.remote.file_detector import LocalFileDetector
from django.core.files.uploadedfile import SimpleUploadedFile
from tempfile import NamedTemporaryFile
class ConfirmWithInlinesTests(AdminConfirmIntegrationTestCase):
def setUp(self):
self.selenium.file_detector = LocalFileDetector()
super().setUp()
def tearDown(self):
reload(shoppingmall_admin)
super().tearDown()
def test_models_without_files_should_not_have_confirmation_received(self):
mall = ShoppingMall.objects.create(name="mall")
self.selenium.get(
self.live_server_url + f"/admin/market/shoppingmall/{mall.id}/change/"
)
# Should ask for confirmation of change
self.assertIn(CONFIRM_CHANGE, self.selenium.page_source)
# Change name
name = self.selenium.find_element_by_name("name")
name.send_keys("New Name")
self.selenium.find_element_by_name("_continue").click()
# Should have hidden form containing the updated name
self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element_by_id("hidden-form")
name = hidden_form.find_element_by_name("name")
self.assertIn("New Name", name.get_attribute("value"))
with self.assertRaises(NoSuchElementException):
self.selenium.find_element_by_name("_confirmation_received")
self.selenium.find_element_by_name("_continue").click()
# Should persist change
mall.refresh_from_db()
self.assertIn("New Name", mall.name)
def test_models_with_files_should_have_confirmation_received(self):
item = Item.objects.create(name="item", price=1)
self.selenium.get(
self.live_server_url + f"/admin/market/item/{item.id}/change/"
)
# Should ask for confirmation of change
self.assertIn(CONFIRM_CHANGE, self.selenium.page_source)
# Change price
price = self.selenium.find_element_by_name("price")
price.send_keys(2)
self.selenium.find_element_by_name("_continue").click()
# Should have hidden form containing the updated price
self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element_by_id("hidden-form")
price = hidden_form.find_element_by_name("price")
self.assertEqual("21.00", price.get_attribute("value"))
self.selenium.find_element_by_name("_confirmation_received")
self.selenium.find_element_by_name("_continue").click()
item.refresh_from_db()
def test_should_save_file_additions(self):
selenium_version = pkg_resources.get_distribution("selenium").parsed_version
if selenium_version.major < 4:
pytest.skip(
"Known issue `https://github.com/SeleniumHQ/selenium/issues/8762` with this selenium version."
)
item = Item.objects.create(
name="item", price=1, currency=Item.VALID_CURRENCIES[0][0]
)
self.selenium.get(
self.live_server_url + f"/admin/market/item/{item.id}/change/"
)
self.assertIn(CONFIRM_CHANGE, self.selenium.page_source)
# Make a change to trigger confirmation page
price = self.selenium.find_element_by_name("price")
price.send_keys(2)
# Upload a new file
self.selenium.find_element_by_id("id_file").send_keys(
os.getcwd() + "/screenshot.png"
)
self.selenium.find_element_by_name("_continue").click()
# Should have hidden form containing the updated price
self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element_by_id("hidden-form")
price = hidden_form.find_element_by_name("price")
self.assertEqual("21.00", price.get_attribute("value"))
self.selenium.find_element_by_name("_confirmation_received")
self.selenium.find_element_by_name("_continue").click()
item.refresh_from_db()
self.assertEqual(21, int(item.price))
self.assertIn("screenshot.png", item.file.name)
def test_should_save_file_changes(self):
selenium_version = pkg_resources.get_distribution("selenium").parsed_version
if selenium_version.major < 4:
pytest.skip(
"Known issue `https://github.com/SeleniumHQ/selenium/issues/8762` with this selenium version."
)
file = SimpleUploadedFile(
name="old_file.jpg",
content=open("screenshot.png", "rb").read(),
content_type="image/jpeg",
)
item = Item.objects.create(
name="item", price=1, currency=Item.VALID_CURRENCIES[0][0], file=file
)
self.selenium.get(
self.live_server_url + f"/admin/market/item/{item.id}/change/"
)
self.assertIn(CONFIRM_CHANGE, self.selenium.page_source)
# Make a change to trigger confirmation page
price = self.selenium.find_element_by_name("price")
price.send_keys(2)
# Upload a new file
self.selenium.find_element_by_id("id_file").send_keys(
os.getcwd() + "/screenshot.png"
)
self.selenium.find_element_by_name("_continue").click()
# Should have hidden form containing the updated price
self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element_by_id("hidden-form")
price = hidden_form.find_element_by_name("price")
self.assertEqual("21.00", price.get_attribute("value"))
self.selenium.find_element_by_name("_confirmation_received")
self.selenium.find_element_by_name("_continue").click()
item.refresh_from_db()
self.assertEqual(21, int(item.price))
self.assertIn("screenshot.png", item.file.name)
def test_should_remove_file_if_clear_selected(self):
file = SimpleUploadedFile(
name="old_file.jpg",
content=open("screenshot.png", "rb").read(),
content_type="image/jpeg",
)
item = Item.objects.create(
name="item", price=1, currency=Item.VALID_CURRENCIES[0][0], file=file
)
self.selenium.get(
self.live_server_url + f"/admin/market/item/{item.id}/change/"
)
self.assertIn(CONFIRM_CHANGE, self.selenium.page_source)
# Make a change to trigger confirmation page
price = self.selenium.find_element_by_name("price")
price.send_keys(2)
# Choose to clear the existing file
self.selenium.find_element_by_id("file-clear_id").click()
self.assertTrue(
self.selenium.find_element_by_xpath(
".//*[@id='file-clear_id']"
).get_attribute("checked")
)
self.selenium.find_element_by_name("_continue").click()
# Should have hidden form containing the updated price
self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element_by_id("hidden-form")
price = hidden_form.find_element_by_name("price")
self.assertEqual("21.00", price.get_attribute("value"))
self.selenium.find_element_by_name("_confirmation_received")
self.selenium.find_element_by_name("_continue").click()
item.refresh_from_db()
self.assertEqual(21, int(item.price))
# Should have cleared `file` since clear was selected
self.assertFalse(item.file)

View File

@ -0,0 +1,202 @@
"""
Tests confirmation of add/change
on ModelAdmin that includes inlines
Does not test confirmation of inline changes
"""
import pytest
import pkg_resources
from importlib import reload
from tests.factories import ShopFactory
from tests.market.models import GeneralManager, ShoppingMall, Town
from admin_confirm.tests.helpers import AdminConfirmIntegrationTestCase
from tests.market.admin import shoppingmall_admin
from admin_confirm.constants import CONFIRM_CHANGE
from selenium.webdriver.support.ui import Select
class ConfirmWithInlinesTests(AdminConfirmIntegrationTestCase):
def setUp(self):
self.admin = shoppingmall_admin.ShoppingMallAdmin
self.admin.inlines = [shoppingmall_admin.ShopInline]
super().setUp()
def tearDown(self):
reload(shoppingmall_admin)
super().tearDown()
def test_should_have_hidden_form(self):
mall = ShoppingMall.objects.create(name="mall")
self.selenium.get(
self.live_server_url + f"/admin/market/shoppingmall/{mall.id}/change/"
)
# Should ask for confirmation of change
self.assertIn(CONFIRM_CHANGE, self.selenium.page_source)
# Change name
name = self.selenium.find_element_by_name("name")
name.send_keys("New Name")
self.selenium.find_element_by_name("_continue").click()
# Should have hidden form containing the updated name
self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element_by_id("hidden-form")
name = hidden_form.find_element_by_name("name")
self.assertIn("New Name", name.get_attribute("value"))
self.selenium.find_element_by_name("_continue").click()
# Should persist change
mall.refresh_from_db()
self.assertIn("New Name", mall.name)
def test_should_have_hidden_formsets(self):
# Not having formsets would cause a `ManagementForm tampered with` issue
gm = GeneralManager.objects.create(name="gm")
shops = [ShopFactory(name=i) for i in range(3)]
town = Town.objects.create(name="town")
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
mall.shops.set(shops)
self.selenium.get(
self.live_server_url + f"/admin/market/shoppingmall/{mall.id}/change/"
)
self.assertIn(CONFIRM_CHANGE, self.selenium.page_source)
# Make a change to trigger confirmation page
name = self.selenium.find_element_by_name("name")
name.send_keys("New Name")
self.selenium.find_element_by_name("_continue").click()
self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element_by_id("hidden-form")
hidden_form.find_element_by_name("ShoppingMall_shops-TOTAL_FORMS")
self.selenium.find_element_by_name("_continue").click()
mall.refresh_from_db()
self.assertIn("New Name", mall.name)
def test_should_have_saved_inline_changes(self):
gm = GeneralManager.objects.create(name="gm")
town = Town.objects.create(name="town")
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
shops = [ShopFactory(name=i) for i in range(3)]
self.selenium.get(
self.live_server_url + f"/admin/market/shoppingmall/{mall.id}/change/"
)
self.assertIn(CONFIRM_CHANGE, self.selenium.page_source)
# Make a change to trigger confirmation page
name = self.selenium.find_element_by_name("name")
name.send_keys("New Name")
# Change shops via inline form
select_shop = Select(
self.selenium.find_element_by_name("ShoppingMall_shops-0-shop")
)
select_shop.select_by_value(str(shops[2].id))
self.selenium.find_element_by_name("_continue").click()
self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element_by_id("hidden-form")
hidden_form.find_element_by_name("ShoppingMall_shops-TOTAL_FORMS")
self.selenium.find_element_by_name("_continue").click()
mall.refresh_from_db()
self.assertIn("New Name", mall.name)
self.assertIn(shops[2], mall.shops.all())
def test_should_respect_get_inlines(self):
# New in Django 3.0
django_version = pkg_resources.get_distribution("Django").parsed_version
if django_version.major < 3:
pytest.skip(
"get_inlines() introducted in Django 3.0, and is not in this version"
)
shoppingmall_admin.ShoppingMallAdmin.inlines = []
shoppingmall_admin.ShoppingMallAdmin.get_inlines = (
lambda self, request, obj=None: [shoppingmall_admin.ShopInline]
)
gm = GeneralManager.objects.create(name="gm")
town = Town.objects.create(name="town")
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
shops = [ShopFactory(name=i) for i in range(3)]
self.selenium.get(
self.live_server_url + f"/admin/market/shoppingmall/{mall.id}/change/"
)
self.assertIn(CONFIRM_CHANGE, self.selenium.page_source)
# Make a change to trigger confirmation page
name = self.selenium.find_element_by_name("name")
name.send_keys("New Name")
# Change shops via inline form
select_shop = Select(
self.selenium.find_element_by_name("ShoppingMall_shops-0-shop")
)
select_shop.select_by_value(str(shops[2].id))
self.selenium.find_element_by_name("_continue").click()
self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element_by_id("hidden-form")
hidden_form.find_element_by_name("ShoppingMall_shops-TOTAL_FORMS")
self.selenium.find_element_by_name("_continue").click()
mall.refresh_from_db()
self.assertIn("New Name", mall.name)
self.assertIn(shops[2], mall.shops.all())
def test_should_respect_get_inline_instances(self):
shoppingmall_admin.ShoppingMallAdmin.inlines = []
shoppingmall_admin.ShoppingMallAdmin.get_inline_instances = (
lambda self, request, obj=None: shoppingmall_admin.ShopInline(
self.model, self.admin_site
)
)
gm = GeneralManager.objects.create(name="gm")
town = Town.objects.create(name="town")
mall = ShoppingMall.objects.create(name="mall", general_manager=gm, town=town)
shops = [ShopFactory(name=i) for i in range(3)]
self.selenium.get(
self.live_server_url + f"/admin/market/shoppingmall/{mall.id}/change/"
)
self.assertIn(CONFIRM_CHANGE, self.selenium.page_source)
# Make a change to trigger confirmation page
name = self.selenium.find_element_by_name("name")
name.send_keys("New Name")
# Change shops via inline form
select_shop = Select(
self.selenium.find_element_by_name("ShoppingMall_shops-0-shop")
)
select_shop.select_by_value(str(shops[2].id))
self.selenium.find_element_by_name("_continue").click()
self.assertIn("Confirm", self.selenium.page_source)
hidden_form = self.selenium.find_element_by_id("hidden-form")
hidden_form.find_element_by_name("ShoppingMall_shops-TOTAL_FORMS")
self.selenium.find_element_by_name("_continue").click()
mall.refresh_from_db()
self.assertIn("New Name", mall.name)
self.assertIn(shops[2], mall.shops.all())

View File

@ -21,8 +21,6 @@ class TestAdminOptions(AdminConfirmTestCase):
mall.shops.set(shops)
# new values
gm2 = GeneralManager.objects.create(name="gm2")
shops2 = [ShopFactory() for i in range(3)]
town2 = Town.objects.create(name="town2")
data = {
@ -92,7 +90,6 @@ class TestAdminOptions(AdminConfirmTestCase):
# new values
gm2 = GeneralManager.objects.create(name="gm2")
shops2 = [ShopFactory() for i in range(3)]
town2 = Town.objects.create(name="town2")
data = {
@ -164,7 +161,6 @@ class TestAdminOptions(AdminConfirmTestCase):
# new values
gm2 = GeneralManager.objects.create(name="gm2")
shops2 = [ShopFactory() for i in range(3)]
town2 = Town.objects.create(name="town2")
data = {
@ -205,7 +201,6 @@ class TestAdminOptions(AdminConfirmTestCase):
# new values
gm2 = GeneralManager.objects.create(name="gm2")
shops2 = [ShopFactory() for i in range(3)]
town2 = Town.objects.create(name="town2")
data = {
@ -246,7 +241,6 @@ class TestAdminOptions(AdminConfirmTestCase):
# new values
gm2 = GeneralManager.objects.create(name="gm2")
shops2 = [ShopFactory() for i in range(3)]
town2 = Town.objects.create(name="town2")
data = {

View File

@ -1,5 +1,4 @@
from unittest import mock
from admin_confirm.admin import AdminConfirmMixin
from django.urls import reverse
from admin_confirm.tests.helpers import AdminConfirmTestCase

View File

@ -170,7 +170,7 @@ class TestFileCache(AdminConfirmTestCase):
self.assertNotIn("You may edit it again below.", message)
# Should have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
self.assertEqual(response.url, "/admin/market/item/")
# Should not have changed existing item
item.refresh_from_db()
@ -290,7 +290,7 @@ class TestFileCache(AdminConfirmTestCase):
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(f"/admin/market/item/add/", data=data)
response = self.client.post("/admin/market/item/add/", data=data)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
@ -299,7 +299,7 @@ class TestFileCache(AdminConfirmTestCase):
self.assertNotIn("You may edit it again below.", message)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
self.assertEqual(response.url, "/admin/market/item/")
# Should not have changed existing item
self.item.refresh_from_db()
@ -353,7 +353,7 @@ class TestFileCache(AdminConfirmTestCase):
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(f"/admin/market/item/add/", data=data)
response = self.client.post("/admin/market/item/add/", data=data)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
@ -362,7 +362,7 @@ class TestFileCache(AdminConfirmTestCase):
self.assertNotIn("You may edit it again below.", message)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
self.assertEqual(response.url, "/admin/market/item/")
# Should not have changed existing item
self.item.refresh_from_db()
@ -398,17 +398,6 @@ class TestFileCache(AdminConfirmTestCase):
"_save": True,
}
# Upload new file
f2 = SimpleUploadedFile(
name="test_file2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Set cache
cache_item = Item(
name=data["name"], price=data["price"], currency=data["currency"], file=f2
)
# Make sure there's no post cached obj
cache.delete(CACHE_KEYS["object"])
cache.set(CACHE_KEYS["post"], data)
@ -418,7 +407,7 @@ class TestFileCache(AdminConfirmTestCase):
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(f"/admin/market/item/add/", data=data)
response = self.client.post("/admin/market/item/add/", data=data)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
@ -427,7 +416,7 @@ class TestFileCache(AdminConfirmTestCase):
self.assertNotIn("You may edit it again below.", message)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
self.assertEqual(response.url, "/admin/market/item/")
# Should not have changed existing item
self.item.refresh_from_db()
@ -463,17 +452,6 @@ class TestFileCache(AdminConfirmTestCase):
"_save": True,
}
# Upload new file
f2 = SimpleUploadedFile(
name="test_file2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Set cache
cache_item = Item(
name=data["name"], price=data["price"], currency=data["currency"], file=f2
)
# Make sure there's no cache
cache.delete(CACHE_KEYS["object"])
cache.delete(CACHE_KEYS["post"])
@ -483,7 +461,7 @@ class TestFileCache(AdminConfirmTestCase):
data[CONFIRMATION_RECEIVED] = True
with mock.patch.object(ItemAdmin, "message_user") as message_user:
response = self.client.post(f"/admin/market/item/add/", data=data)
response = self.client.post("/admin/market/item/add/", data=data)
# Should show message to user with correct obj and path
message_user.assert_called_once()
message = message_user.call_args[0][1]
@ -492,7 +470,7 @@ class TestFileCache(AdminConfirmTestCase):
self.assertNotIn("You may edit it again below.", message)
# Should not have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
self.assertEqual(response.url, "/admin/market/item/")
# Should not have changed existing item
self.item.refresh_from_db()
@ -571,7 +549,7 @@ class TestFileCache(AdminConfirmTestCase):
self.assertNotIn("You may edit it again below.", message)
# Should have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
self.assertEqual(response.url, "/admin/market/item/")
# Should not have changed existing item
item.refresh_from_db()
@ -601,12 +579,6 @@ class TestFileCache(AdminConfirmTestCase):
# Load the Change Item Page
ItemAdmin.save_as_continue = False
# Upload new image and remove file
i2 = SimpleUploadedFile(
name="test_image2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Request.POST
data = {
"id": item.id,
@ -619,14 +591,6 @@ class TestFileCache(AdminConfirmTestCase):
"_saveasnew": True,
}
# Set cache
cache_item = Item(
name=data["name"],
price=data["price"],
currency=data["currency"],
image=i2,
)
# Ensure no cached obj
cache.delete(CACHE_KEYS["object"])
cache.set(CACHE_KEYS["post"], data)
@ -647,7 +611,7 @@ class TestFileCache(AdminConfirmTestCase):
self.assertNotIn("You may edit it again below.", message)
# Should have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
self.assertEqual(response.url, "/admin/market/item/")
# Should not have changed existing item
item.refresh_from_db()
@ -676,12 +640,6 @@ class TestFileCache(AdminConfirmTestCase):
# Load the Change Item Page
ItemAdmin.save_as_continue = False
# Upload new image and remove file
i2 = SimpleUploadedFile(
name="test_image2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Request.POST
data = {
"id": item.id,
@ -694,14 +652,6 @@ class TestFileCache(AdminConfirmTestCase):
"_saveasnew": True,
}
# Set cache
cache_item = Item(
name=data["name"],
price=data["price"],
currency=data["currency"],
image=i2,
)
# Ensure no cache
cache.delete(CACHE_KEYS["object"])
cache.delete(CACHE_KEYS["post"])
@ -722,7 +672,7 @@ class TestFileCache(AdminConfirmTestCase):
self.assertNotIn("You may edit it again below.", message)
# Should have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
self.assertEqual(response.url, "/admin/market/item/")
# Should not have changed existing item
item.refresh_from_db()
@ -790,7 +740,7 @@ class TestFileCache(AdminConfirmTestCase):
self.assertNotIn("You may edit it again below.", message)
# Should have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
self.assertEqual(response.url, "/admin/market/item/")
# Should have changed existing item
self.assertEqual(Item.objects.count(), 1)
@ -890,12 +840,6 @@ class TestFileCache(AdminConfirmTestCase):
# Load the Change Item Page
ItemAdmin.save_as_continue = False
# Upload new image and remove file
i2 = SimpleUploadedFile(
name="test_image2.jpg",
content=open(self.image_path, "rb").read(),
content_type="image/jpeg",
)
# Request.POST
data = {
"id": item.id,
@ -930,7 +874,7 @@ class TestFileCache(AdminConfirmTestCase):
self.assertNotIn("You may edit it again below.", message)
# Should have redirected to changelist
self.assertEqual(response.url, f"/admin/market/item/")
self.assertEqual(response.url, "/admin/market/item/")
# Should have changed existing item
self.assertEqual(Item.objects.count(), 1)

View File

@ -2,7 +2,13 @@ version: "3.9"
services:
web:
build: .
build:
context: .
dockerfile: Dockerfile
args:
PYTHON_VERSION: "$PYTHON_VERSION"
DJANGO_VERSION: "$DJANGO_VERSION"
SELENIUM_VERSION: "$SELENIUM_VERSION"
command: python tests/manage.py runserver 0.0.0.0:8000
volumes:
- .:/code
@ -16,3 +22,5 @@ services:
ports:
- "4444:4444" # Selenium
- "5900:5900" # VNC
volumes:
- .:/code

View File

@ -1,4 +0,0 @@
[pytest]
DJANGO_SETTINGS_MODULE=tests.test_project.settings.test
addopts = --doctest-modules -ra -l --tb=short --show-capture=stdout --color=yes
testpaths = admin_confirm

View File

@ -1,6 +1,5 @@
Django>=1.7.0
factory-boy~=3.0.1
django-admin-confirm~=0.2.2
coverage~=5.4
pytest~=6.2.2
pytest-django~=4.1.0
@ -8,4 +7,10 @@ readme-renderer~=28.0
twine~=3.3.0
coveralls~=3.0.0
Pillow~=8.1.0 # For ImageField
selenium~=3.141.0
# Known issue: https://github.com/SeleniumHQ/selenium/issues/8762
# Python 3.6 should use because selenium 4 doesn't work with py3.6
# selenium~=3.141.0
# Others should use
selenium~=4.0.0.a5

26
setup.cfg 100644
View File

@ -0,0 +1,26 @@
[flake8]
max-complexity = 10
max-line-length = 127
paths = admin_confirm
exclude =
admin_confirm/tests/*
tests/*
ignore =
C812 # missing trailing comma
I001 # isort found an import in the wrong position
I004 # sisort found an unexpected blank line in imports
Q000 # Remove bad quotes
WPS110 # Seems to require no one word variable names
WPS305 # Found f string
WPS336 # Explicit string concatination
per-file-ignores =
admin_confirm/tests/*: D102, WPS118, WPS204
[coverage:run]
relative_files = True
omit = admin_confirm/tests/*
branch = True
[tool:pytest]
DJANGO_SETTINGS_MODULE=tests.test_project.settings.test
addopts = --doctest-modules -ra -l --tb=short --show-capture=stdout --color=yes
testpaths = admin_confirm

View File

@ -1,4 +1,5 @@
import os
from setuptools import setup
here = os.path.abspath(os.path.dirname(__file__))
@ -8,7 +9,7 @@ setup(
name="django-admin-confirm",
version="0.2.3.dev9",
packages=["admin_confirm"],
description="Adds confirmation to Django Admin changes, additions and actions",
description=("Adds confirmation to Django Admin changes, additions and actions"),
long_description_content_type="text/markdown",
long_description=README,
author="Thu Trang Pham",

View File

@ -6,13 +6,15 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('market', '0003_auto_20201108_1717'),
("market", "0003_auto_20201108_1717"),
]
operations = [
migrations.AddField(
model_name='inventory',
name='notes',
field=models.TextField(blank=True, default='This is the default', null=True),
model_name="inventory",
name="notes",
field=models.TextField(
blank=True, default="This is the default", null=True
),
),
]

View File

@ -6,16 +6,24 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('market', '0004_inventory_notes'),
("market", "0004_inventory_notes"),
]
operations = [
migrations.CreateModel(
name='ShoppingMall',
name="ShoppingMall",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=120)),
('shops', models.ManyToManyField(to='market.Shop')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=120)),
("shops", models.ManyToManyField(to="market.Shop")),
],
),
]

View File

@ -7,42 +7,68 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('market', '0005_shoppingmall'),
("market", "0005_shoppingmall"),
]
operations = [
migrations.CreateModel(
name='GeneralManager',
name="GeneralManager",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=120)),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=120)),
],
),
migrations.CreateModel(
name='Town',
name="Town",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=120)),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=120)),
],
),
migrations.AddField(
model_name='item',
name='file',
field=models.FileField(blank=True, null=True, upload_to='tmp/files'),
model_name="item",
name="file",
field=models.FileField(blank=True, null=True, upload_to="tmp/files"),
),
migrations.AddField(
model_name='item',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='tmp/items'),
model_name="item",
name="image",
field=models.ImageField(blank=True, null=True, upload_to="tmp/items"),
),
migrations.AddField(
model_name='shoppingmall',
name='general_manager',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='market.generalmanager'),
model_name="shoppingmall",
name="general_manager",
field=models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="market.generalmanager",
),
),
migrations.AddField(
model_name='shoppingmall',
name='town',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='market.town'),
model_name="shoppingmall",
name="town",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="market.town",
),
),
]

View File

@ -6,13 +6,15 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('market', '0006_auto_20210222_0312'),
("market", "0006_auto_20210222_0312"),
]
operations = [
migrations.AddField(
model_name='generalmanager',
name='headshot',
field=models.ImageField(blank=True, null=True, upload_to='tmp/gm/headshots'),
model_name="generalmanager",
name="headshot",
field=models.ImageField(
blank=True, null=True, upload_to="tmp/gm/headshots"
),
),
]

View File

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('market', '0007_generalmanager_headshot'),
("market", "0007_generalmanager_headshot"),
]
operations = [
migrations.AddField(
model_name='item',
name='description',
model_name="item",
name="description",
field=models.TextField(blank=True, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-03-04 03:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("market", "0008_item_description"),
]
operations = [
migrations.AlterField(
model_name="shoppingmall",
name="shops",
field=models.ManyToManyField(blank=True, null=True, to="market.Shop"),
),
]

View File

@ -49,7 +49,7 @@ class Town(models.Model):
class ShoppingMall(models.Model):
name = models.CharField(max_length=120)
shops = models.ManyToManyField(Shop)
shops = models.ManyToManyField(Shop, blank=True, null=True)
general_manager = models.OneToOneField(
GeneralManager, on_delete=models.CASCADE, null=True, blank=True
)

View File

@ -24,13 +24,13 @@ SECRET_KEY = "=yddl-40388w3e2hl$e8)revce=n67_idi8pfejtn3!+2%!_qt"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
USE_DCOKER = os.environ.get("USE_DOCKER", '').lower() == "true"
USE_DCOKER = os.environ.get("USE_DOCKER", "").lower() == "true"
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
if USE_DCOKER:
import socket
ALLOWED_HOSTS = [socket.gethostbyname(socket.gethostname())]
ALLOWED_HOSTS += [socket.gethostbyname(socket.gethostname())]
# Application definition

View File

@ -1,5 +1,5 @@
from .base import *
INSTALLED_APPS = INSTALLED_APPS + ['market']
INSTALLED_APPS = INSTALLED_APPS + ["market"]
WSGI_APPLICATION = "test_project.wsgi.application"
ROOT_URLCONF = "test_project.urls"

View File

@ -1,5 +1,5 @@
from .base import *
INSTALLED_APPS = INSTALLED_APPS + ['tests.market']
INSTALLED_APPS = INSTALLED_APPS + ["tests.market"]
WSGI_APPLICATION = "tests.test_project.wsgi.application"
ROOT_URLCONF = "tests.test_project.urls"