Skip to content

Commit

Permalink
Adding MVP file type input feature (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
DiogenesAnalytics committed Mar 14, 2024
1 parent b93298b commit 668ab31
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 39 deletions.
9 changes: 9 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
"form_backend_url": null,
"email": "[email protected]",
"questions": [
{
"label": "Upload funny memes",
"name": "meme_imgs",
"type": "file",
"required": true,
"custom": {
"multiple": true
}
},
{
"label": "Select your favorite color",
"name": "favorite_color",
Expand Down
36 changes: 24 additions & 12 deletions scripts/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,18 @@ function generateHtmlContent(formData) {
<style>
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
background-color: #222;
color: #eee;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: #fff;
background-color: #333;
color: #eee;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.1);
}
label {
font-weight: bold;
Expand All @@ -100,16 +102,16 @@ function generateHtmlContent(formData) {
</head>
<body>
<div class="container">
<h2>Contact Form Response</h2>`;
<h2>Contact Form Response</h2>\n`;

// Add form data to the HTML content
for (const key in formData) {
htmlContent += `<label for="${key}">${key}:</label>`;
htmlContent += `<label for="${key}">${key}:</label>\n`;

// If the value is an array (multiple files)
if (Array.isArray(formData[key])) {
// First add some extra space between label and value(s)
htmlContent += `<p>`;
htmlContent += `<p>\n`;

// Handle multiple file data URLs and the correct tag for the MIME type
formData[key].forEach(url => {
Expand All @@ -119,7 +121,7 @@ function generateHtmlContent(formData) {
const mimeType = url.split(';')[0].split(':')[1];

if (mimeType.startsWith('image')) {
htmlContent += `<img src="${url}" alt="${key}" /><br>`;
htmlContent += `<img src="${url}" alt="${key}" /><br>\n`;
} else if (mimeType.startsWith('video')) {
htmlContent += `
<video controls>
Expand All @@ -135,16 +137,16 @@ function generateHtmlContent(formData) {
</audio><br>
`;
} else {
htmlContent += `<p><a href="${url}">${key}</a></p>`;
htmlContent += `<a href="${url}">${key}</a>\n`;
}
});

// Close <p> tag
htmlContent += `</p>`;
htmlContent += `</p>\n`;

} else {
// Just normal data
htmlContent += `<p>${formData[key]}</p>`;
htmlContent += `<p>${formData[key]}</p>\n`;
}
}

Expand Down Expand Up @@ -246,8 +248,18 @@ fetch('config.json')
// Set form backend URL if available and not null
const formBackendUrl = data.form_backend_url;
if (formBackendUrl !== undefined && formBackendUrl !== null) {
form.setAttribute('action', formBackendUrl);
form.setAttribute('enctype', 'application/x-www-form-urlencoded');
// Set form backend URL and enctype if available and not null
form.setAttribute('action', formBackendUrl);

// Check if any question has type="file" in config.json
const hasFileUpload = data.questions.some(question => question.type === 'file');
if (hasFileUpload) {
// If any question involves file upload use multipart encoding
form.setAttribute('enctype', 'multipart/form-data');
} else {
// Otherwise, set enctype for URL encoding
form.setAttribute('enctype', 'application/x-www-form-urlencoded');
}
}

// Update email placeholder in instructions
Expand Down
105 changes: 86 additions & 19 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
from typing import Any
from typing import Dict
from typing import Generator
from typing import Set
from typing import Tuple

import pytest
from flask import Flask
from flask import render_template
from flask import request
from flask import send_from_directory
from PIL import Image
from selenium.webdriver.common.keys import Keys
from werkzeug.datastructures import FileStorage
from werkzeug.datastructures import ImmutableMultiDict
Expand All @@ -32,6 +34,25 @@ def pytest_configure(config):
config.addinivalue_line("markers", "website: custom marker for website tests.")


def generate_unique_random_ports(num_ports: int) -> Generator[int, None, None]:
"""Generator that only yield unique random ports."""
# create set of used ports
used_ports: Set[int] = set()

# loop over ports
while len(used_ports) < num_ports:
# get random port
port = random.randint(5001, 65535)

# check it is unique
if port not in used_ports:
# send it forward
yield port

# mark it as used
used_ports.add(port)


def create_temp_websrc_dir(src: Path, dst: Path, src_files: Tuple[str, ...]) -> Path:
"""Create and populate a temporary directory with static web source files."""
# create new destination subdir
Expand Down Expand Up @@ -182,6 +203,9 @@ def submit_form():
# process any files
process_uploaded_files(processed_data)

# log files added
print(f"Added uploaded files: {processed_data}")

# now render response
return render_template("form_response_template.html", form_data=processed_data)

Expand Down Expand Up @@ -212,17 +236,35 @@ def write_config_file(config: Dict[str, Any], src_path: Path) -> None:
json.dump(config, json_file, indent=2)


def update_form_backend_config(
config: Dict[str, Any], src_path: Path, port: int
) -> None:
"""Set the form backend url to testing server url."""
def prepare_default_config(config: Dict[str, Any], src_path: Path, port: int) -> None:
"""Update the default config copy with values approprate for testing."""
# update form backend
config["form_backend_url"] = f"http://localhost:{port}/submit"

# update input[type=file] accept attr
for question in config["questions"]:
# check type
if question["type"] == "file":
# check for custom attr
if "custom" in question:
# only update accept attr
question["custom"]["accept"] = "*"

else:
# add custom section with accept attr
question["custom"] = {"accept": "*"}

# write out updated file
write_config_file(config, src_path)


@pytest.fixture(scope="session")
def session_tmp_dir(tmp_path_factory) -> Path:
"""Uses temporary path factory to create a session-scoped temp path."""
# create a temporary directory using tmp_path_factory
return tmp_path_factory.mktemp("session_temp_dir")


@pytest.fixture(scope="function")
def dummy_txt_file_path(tmp_path) -> Path:
"""Create a dummy temporary text file."""
Expand Down Expand Up @@ -273,17 +315,47 @@ def dummy_form_post_data(dummy_txt_file_stream) -> Dict[str, Any]:


@pytest.fixture(scope="session")
def form_inputs() -> Dict[str, Any]:
def dummy_jpg_file_path(session_tmp_dir: Path) -> Path:
"""Create a dummy JPEG image."""
# create image dir
img_dir = session_tmp_dir / "images"
img_dir.mkdir()

# create a dummy image
img_path = img_dir / "dummy_image.jpg"
image = Image.new("RGB", (100, 100), color="red") # create a red image
image.save(img_path)

return img_path


@pytest.fixture(scope="session")
def dummy_jpg_data_url(dummy_jpg_file_path) -> str:
"""Create a data URL for the dummy JPEG file."""
# read the content of the file
with open(dummy_jpg_file_path, "rb") as f:
file_content = f.read()

# encode the file content as base64
base64_content = base64.b64encode(file_content).decode("utf-8")

# construct the data URL with the appropriate MIME type
return f"data:image/jpeg;base64,{base64_content}"


@pytest.fixture(scope="session")
def form_inputs(dummy_jpg_file_path: Path, dummy_jpg_data_url: str) -> Dict[str, Any]:
"""Defines the values to be submitted for each input type during form tests."""
return {
"email": "[email protected]",
"date": {"date": "01012000"},
"datetime-local": {
"date": "01012000",
"tab": Keys.TAB,
"time": "1200",
"period": "AM",
},
"email": "[email protected]",
"file": (str(dummy_jpg_file_path), dummy_jpg_data_url),
"number": "42",
"selectbox": None,
"tel": "18005554444",
Expand Down Expand Up @@ -317,13 +389,6 @@ def website_files() -> Tuple[str, ...]:
return ("index.html", "config.json", "styles", "scripts")


@pytest.fixture(scope="session")
def session_tmp_dir(tmp_path_factory) -> Path:
"""Uses temporary path factory to create a session-scoped temp path."""
# create a temporary directory using tmp_path_factory
return tmp_path_factory.mktemp("session_temp_dir")


@pytest.fixture(scope="session")
def session_websrc_tmp_dir(
project_directory: Path, session_tmp_dir: Path, website_files: Tuple[str, ...]
Expand Down Expand Up @@ -375,7 +440,7 @@ def session_web_app(
port = 5000

# now update config.json with new backend url
update_form_backend_config(default_site_config, session_websrc_tmp_dir, port)
prepare_default_config(default_site_config, session_websrc_tmp_dir, port)

# create app
return build_flask_app(session_websrc_tmp_dir, port, submit_route)
Expand All @@ -396,18 +461,20 @@ def live_session_web_app_url(session_web_app: Flask) -> str:


@pytest.fixture(scope="function")
def random_port() -> int:
"""Generate a random port greater than 5000."""
return random.randint(5001, 65535)
def unique_random_port() -> int:
"""Generate a unique random port greater than 5000."""
# control the number of ports generated
num_ports = 100
return next(generate_unique_random_ports(num_ports))


@pytest.fixture(scope="function")
def function_web_app(
function_websrc_tmp_dir: Path, submit_route: str, random_port: int
function_websrc_tmp_dir: Path, submit_route: str, unique_random_port: int
) -> Flask:
"""Create a function-scoped Flask app for testing with the website source."""
# create app
return build_flask_app(function_websrc_tmp_dir, random_port, submit_route)
return build_flask_app(function_websrc_tmp_dir, unique_random_port, submit_route)


@pytest.fixture(scope="function")
Expand Down
6 changes: 6 additions & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ attrs==23.2.0
beautifulsoup4==4.12.3
behave==1.2.6
black==24.2.0
blinker==1.7.0
certifi==2024.2.2
chardet==5.2.0
charset-normalizer==3.3.2
Expand All @@ -20,7 +21,10 @@ h11==0.14.0
idna==3.6
iniconfig==2.0.0
isort==5.13.2
itsdangerous==2.1.2
Jinja2==3.1.3
markdown-it-py==3.0.0
MarkupSafe==2.1.5
mccabe==0.7.0
mdurl==0.1.2
mypy==1.8.0
Expand All @@ -33,6 +37,7 @@ parse-type==0.6.2
pathspec==0.12.1
pdbp==1.5.0
pep8-naming==0.13.3
pillow==10.2.0
platformdirs==4.2.0
pluggy==1.4.0
py==1.11.0
Expand Down Expand Up @@ -65,4 +70,5 @@ trio==0.24.0
trio-websocket==0.11.1
typing_extensions==4.10.0
urllib3==2.2.1
Werkzeug==3.0.1
wsproto==1.2.0
Loading

0 comments on commit 668ab31

Please sign in to comment.