Skip to content

Commit

Permalink
Merge branch 'master' into add-uri-filters
Browse files Browse the repository at this point in the history
  • Loading branch information
dmos62 committed Feb 28, 2022
2 parents f2fd9da + 02e22cb commit 90b4ee1
Show file tree
Hide file tree
Showing 18 changed files with 279 additions and 38 deletions.
4 changes: 4 additions & 0 deletions mathesar/api/display_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
{"name": "locale", "type": "string"}]
},
MathesarTypeIdentifier.DATETIME.value:
{
"options": [{"name": "format", "type": "string"}]
},
MathesarTypeIdentifier.DURATION.value:
{
"options": [{"name": "format", "type": "string"}]
}
Expand Down
16 changes: 15 additions & 1 deletion mathesar/api/serializers/shared_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def validate(self, datetime_obj, display_format, serializer_field):
class TimeWithTimeZoneFormatValidator(AbstractDateTimeFormatValidator):

def validate(self, datetime_obj, display_format, serializer_field):
time_only_format = 'HH:mm:ssZZ'
time_only_format = 'HH:mm:ss.SSSZZ'
time_str = arrow.get('2013-09-30T15:34:00.000-07:00').format(time_only_format)
parsed_time_str = arrow.get(time_str, time_only_format)
if parsed_time_str.date() != datetime_obj.date():
Expand All @@ -180,6 +180,15 @@ def validate(self, datetime_obj, display_format, serializer_field):
return super().validate(datetime_obj, display_format, serializer_field)


class DurationFormatValidator(AbstractDateTimeFormatValidator):

def validate(self, datetime_obj, display_format, serializer_field):
if 'z' in display_format.lower():
raise serializers.ValidationError(
"Duration column cannot contain timezone display format"
)


class DateDisplayOptionSerializer(MathesarErrorMessageMixin, OverrideRootPartialMixin, serializers.Serializer):
format = serializers.CharField(validators=[DateFormatValidator()])

Expand Down Expand Up @@ -216,6 +225,10 @@ class TimeWithoutTimezoneDisplayOptionSerializer(
format = serializers.CharField(validators=[TimeWithoutTimeZoneFormatValidator()])


class DurationDisplayOptionSerializer(MathesarErrorMessageMixin, OverrideRootPartialMixin, serializers.Serializer):
format = serializers.CharField(validators=[DurationFormatValidator()])


class DisplayOptionsMappingSerializer(
MathesarErrorMessageMixin,
ReadWritePolymorphicSerializerMappingMixin,
Expand All @@ -231,6 +244,7 @@ class DisplayOptionsMappingSerializer(
('date', MathesarTypeIdentifier.DATETIME.value): DateDisplayOptionSerializer,
('time with time zone', MathesarTypeIdentifier.DATETIME.value): TimeWithTimezoneDisplayOptionSerializer,
('time without time zone', MathesarTypeIdentifier.DATETIME.value): TimeWithoutTimezoneDisplayOptionSerializer,
MathesarTypeIdentifier.DURATION.value: DurationDisplayOptionSerializer,
}

def get_mapping_field(self):
Expand Down
11 changes: 0 additions & 11 deletions mathesar/tests/api/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,6 @@ def _create_data_file(file_path, file_name):
return _create_data_file


@pytest.fixture
def create_table(csv_filename, create_schema):
with open(csv_filename, 'rb') as csv_file:
data_file = DataFile.objects.create(file=File(csv_file))

def _create_table(table_name, schema='Patents'):
schema_model = create_schema(schema)
return create_table_from_csv(data_file, table_name, schema_model)
return _create_table


@pytest.fixture
def create_data_types_table(data_types_csv_filename, create_schema):
with open(data_types_csv_filename, 'rb') as csv_file:
Expand Down
1 change: 1 addition & 0 deletions mathesar/tests/api/test_column_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ def test_column_create_invalid_default(column_test_table, client):
("BOOLEAN", {"input": "dropdown"}),
("BOOLEAN", {"input": "checkbox", "custom_labels": {"TRUE": "yes", "FALSE": "no"}}),
("DATE", {'format': 'YYYY-MM-DD'}),
("INTERVAL", {'format': 'DD HH:mm:ss.SSS'}),
("NUMERIC", {"show_as_percentage": True}),
("NUMERIC", {"show_as_percentage": True, "locale": "en_US"}),
("TIMESTAMP WITH TIME ZONE", {'format': 'YYYY-MM-DD hh:mm'}),
Expand Down
17 changes: 17 additions & 0 deletions mathesar/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
from django.core.files import File
"""
This inherits the fixtures in the root conftest.py
"""
import pytest

from sqlalchemy import text

from db.schemas.operations.create import create_schema as create_sa_schema
from db.schemas.utils import get_schema_oid_from_name, get_schema_name_from_oid
from mathesar.imports.csv import create_table_from_csv
from mathesar.models import Database
from mathesar.models import Schema, DataFile

from sqlalchemy import Column, MetaData, text, Integer
from sqlalchemy import Table as SATable
Expand Down Expand Up @@ -155,3 +162,13 @@ def empty_nasa_table(patent_schema):

table.delete_sa_table()
table.delete()

@pytest.fixture
def create_table(csv_filename, create_schema):
with open(csv_filename, 'rb') as csv_file:
data_file = DataFile.objects.create(file=File(csv_file))

def _create_table(table_name, schema='Patents'):
schema_model = create_schema(schema)
return create_table_from_csv(data_file, table_name, schema_model)
return _create_table
21 changes: 21 additions & 0 deletions mathesar/tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from db.schemas.operations.create import create_schema as create_sa_schema
from db.schemas.utils import get_schema_name_from_oid, get_schema_oid_from_name
from mathesar.models import Database, Schema
from mathesar.tests.integration.utils.locators import get_table_entry

TEST_SCHEMA = 'import_csv_schema'
PATENT_SCHEMA = 'Patents'
Expand Down Expand Up @@ -67,3 +68,23 @@ def schema(create_schema, schema_name):
@pytest.fixture
def base_schema_url(schema, live_server):
return f"{live_server}/{schema.database.name}/{schema.id}"


@pytest.fixture
def schemas_page_url(live_server, test_db_name):
return f"{live_server}/{test_db_name}/schemas/"


@pytest.fixture
def go_to_patents_data_table(page, create_table, schema_name, base_schema_url):
"""
Imports the `patents.csv` data into a table named "patents" and navigates to
the view of that table before starting the test.
"""
table_name = "patents"
table = create_table(table_name, schema_name)
table.import_verified = True
table.save()
page.goto(base_schema_url)
get_table_entry(page, table_name).click()
yield table_name
10 changes: 10 additions & 0 deletions mathesar/tests/integration/test_columns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from playwright.sync_api import expect


def test_add_column(page, go_to_patents_data_table):
page.click("button[aria-label='New Column']")
column_name = "TEST"
page.fill(".new-column-dropdown input", column_name)
page.click("button:has-text('Add')")
column_header = f".table-content .header .cell .name:has-text('{column_name}')"
expect(page.locator(column_header)).to_be_visible()
32 changes: 32 additions & 0 deletions mathesar/tests/integration/test_constraints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from playwright.sync_api import expect


def test_add_and_remove_multi_column_unique_constraint(page, go_to_patents_data_table):
# Add
page.click("[aria-label='Table Actions']")
page.click("text=Constraints")
page.click("[aria-label='New Constraint']")
page.click("button:has-text('Unique')")
page.click("fieldset >> text=Center")
page.click("fieldset >> text=Case Number")
page.click("button:has-text('Add')")
column_names = "Center, Case Number"
constraint = page.locator(
f".table-constraint:has(.type:has-text('unique')):has(.columns:has-text('{column_names}'))"
)
expect(page.locator("text=Table Constraints (2)")).to_be_visible()
expect(constraint).to_be_visible()

# Remove
constraint.locator(".drop button").click()
page.click("button:has-text('Delete Constraint')")
expect(constraint).not_to_be_visible()
expect(page.locator("text=Table Constraints (1)")).to_be_visible()


def test_try_to_dissallow_null_for_column_with_null_values(page, go_to_patents_data_table):
page.click("button:has-text('Patent Number')")
allow_null = page.locator("button:has-text('Allow NULL')")
allow_null.click()
expect(page.locator(".toast-item:has-text('Unable to update')")).to_be_visible()
expect(allow_null).to_be_visible()
36 changes: 14 additions & 22 deletions mathesar/tests/integration/test_imports.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
from playwright.sync_api import Page, expect
from playwright.sync_api import expect

from mathesar.tests.integration.utils.locators import get_table_entry, get_tables_list

def get_tables_list(page: Page):
return page.locator("#sidebar li[aria-level='1']:has(button:has-text('Tables')) ul")


def get_table_entry(tables_list, table_name):
return tables_list.locator(f"li:has-text('{table_name}')")


def test_create_empty_table(page: Page, base_schema_url):
def test_import_from_clipboard(page, base_schema_url):
page.goto(base_schema_url)
tables_list = get_tables_list(page)
expect(tables_list).to_be_empty()
expect(get_tables_list(page)).to_be_empty()
page.click("[aria-label='New Table']")
page.click("button:has-text('Empty Table')")
table_entry = get_table_entry(tables_list, "Table 0")
expect(table_entry).to_be_visible()
page.click("button:has-text('Import Data')")
page.click("text=Copy and Paste Text")
page.fill("textarea", "foo,bar\n2,3")
page.click("button:has-text('Continue')")
page.click("button:has-text('Finish Import')")
expect(get_table_entry(page, "Table 0")).to_be_visible()


def test_import_from_clipboard(page: Page, base_schema_url):
def test_import_from_file(page, base_schema_url):
page.goto(base_schema_url)
tables_list = get_tables_list(page)
expect(tables_list).to_be_empty()
page.click("[aria-label='New Table']")
page.click("button:has-text('Import Data')")
page.click("text=Copy and Paste Text")
page.fill("textarea", "foo,bar\n2,3")
page.click("button:has-text('Continue')")
page.set_input_files(".file-upload input", "/code/mathesar/tests/data/patents.csv")
page.click("button:has-text('Finish Import')")
table_entry = get_table_entry(tables_list, "Table 0")
expect(table_entry).to_be_visible()
# "1393 records" is part of the text shown below the table near the pager
expect(page.locator("text=1393 records")).to_be_visible()
49 changes: 49 additions & 0 deletions mathesar/tests/integration/test_records.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import re

from playwright.sync_api import expect

first_pk_cell_in_table = ".row .cell.is-pk >> nth=0"


def test_add_row(page, go_to_patents_data_table):
# Note: This is the New Record button at the top of the table. I also tried
# to write a separate test for adding a row from the row placeholder at the
# bottom of the table, but that proved difficult to write because of the
# VirtualList so I abandoned it.
page.click("button:has-text('New Record')")
expect(page.locator(".row.done .cell.is-pk:has-text('1394')")).to_be_visible()


def test_sort_table_by_column(page, go_to_patents_data_table):
page.click("button:has-text('Title')")
page.click("button:has-text('Sort Descending')")
page.click("button:has-text('Status')")
page.click("button:has-text('Sort Ascending')")
expect(page.locator(first_pk_cell_in_table)).to_have_text("729")


def test_increment_pagination(page, go_to_patents_data_table):
page.click("[aria-label='Goto Page 2']")
expect(page.locator(first_pk_cell_in_table)).to_have_text("501")


def test_edit_cell(page, go_to_patents_data_table):
row = page.locator(".row:has-text('ARC-14231-3')")
cell = row.locator(".cell:has-text('Issued')")
input = cell.locator("input")
all_changes_saved = page.locator("text=All changes saved")
cell.dblclick()
input.fill("TEST")
page.keyboard.press("Enter")
expect(all_changes_saved).to_be_visible()
expect(row).to_have_class(re.compile("updated"))


def test_delete_multiple_rows(page, go_to_patents_data_table):
page.hover(".row:has-text('ARC-14281-1')")
page.check(".row:has-text('ARC-14281-1') input[type='checkbox']")
page.hover(".row:has-text('ARC-14512-1')")
page.check(".row:has-text('ARC-14512-1') input[type='checkbox']")
page.click("button:has-text('Delete 2 records')")
expect(page.locator("text=ARC-14281-1")).not_to_be_visible()
expect(page.locator("text=ARC-14512-1")).not_to_be_visible()
18 changes: 18 additions & 0 deletions mathesar/tests/integration/test_schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from playwright.sync_api import expect


def test_create_and_delete_schema(page, schemas_page_url):
page.goto(schemas_page_url)
schema_name = "foo"
schema_entry = page.locator(f".schema-list .schema-row:has-text('{schema_name}')")
expect(schema_entry).not_to_be_visible()
page.click("text=New Schema")
page.fill("[aria-label='name']", schema_name)
page.click("button:has-text('Save')")
expect(schema_entry).to_be_visible()
# We're also deleting the schema in the same test as a way of cleaning up
# the state created in this test so as not to interfer with other tests.
# This is a hack for now.
schema_entry.locator("button[aria-label='Delete Schema']").click()
page.click("button:has-text('Delete Schema')")
expect(schema_entry).not_to_be_visible()
11 changes: 11 additions & 0 deletions mathesar/tests/integration/test_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from playwright.sync_api import expect

from mathesar.tests.integration.utils.locators import get_table_entry, get_tables_list


def test_create_empty_table(page, base_schema_url):
page.goto(base_schema_url)
expect(get_tables_list(page)).to_be_empty()
page.click("[aria-label='New Table']")
page.click("button:has-text('Empty Table')")
expect(get_table_entry(page, "Table 0")).to_be_visible()
72 changes: 72 additions & 0 deletions mathesar/tests/integration/test_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import re
from playwright.sync_api import expect, Locator
from mathesar.tests.integration.utils.locators import get_table_entry


def create_new_empty_table(page):
page.click("[aria-label='New Table']")
page.click("button:has-text('Empty Table')")


def get_tab(page, tab_text):
return page.locator(
f".tab-container [role=tablist] [role=presentation]:has-text('{tab_text}')"
)


def close_tab(tab_locator: Locator):
tab_locator.hover()
tab_locator.locator("[aria-label=remove]").click()


def test_tabs(page, base_schema_url):
page.goto(base_schema_url)

# Create Table 0
create_new_empty_table(page)
table_0_entry = get_table_entry(page, "Table 0")
table_0_tab = get_tab(page, "Table 0")
close_tab(table_0_tab)

# Create Table 1
create_new_empty_table(page)
table_1_entry = get_table_entry(page, "Table 1")
table_1_tab = get_tab(page, "Table 1")
close_tab(table_1_tab)

# No tabs should be open
expect(table_0_tab).not_to_be_visible()
expect(table_1_tab).not_to_be_visible()

# Open Table 0
table_0_entry.click()
expect(table_0_entry.locator(".item")).to_have_class(re.compile("active"))
expect(table_0_tab).to_have_class(re.compile("active"))

# Open Table 1
table_1_entry.click()
expect(table_0_entry.locator(".item")).not_to_have_class(re.compile("active"))
expect(table_1_entry.locator(".item")).to_have_class(re.compile("active"))
expect(table_0_tab).not_to_have_class(re.compile("active"))
expect(table_1_tab).to_have_class(re.compile("active"))

# Switch to tab for Table 0
table_0_tab.click()
expect(table_0_entry.locator(".item")).to_have_class(re.compile("active"))
expect(table_1_entry.locator(".item")).not_to_have_class(re.compile("active"))
expect(table_0_tab).to_have_class(re.compile("active"))
expect(table_1_tab).not_to_have_class(re.compile("active"))

# Close tab for Table 0
close_tab(table_0_tab)
expect(table_0_entry.locator(".item")).not_to_have_class(re.compile("active"))
expect(table_1_entry.locator(".item")).to_have_class(re.compile("active"))
expect(table_0_tab).not_to_be_visible()
expect(table_1_tab).to_have_class(re.compile("active"))

# Close tab for Table 1
close_tab(table_1_tab)
expect(table_0_entry.locator(".item")).not_to_have_class(re.compile("active"))
expect(table_1_entry.locator(".item")).not_to_have_class(re.compile("active"))
expect(table_0_tab).not_to_be_visible()
expect(table_1_tab).not_to_be_visible()
6 changes: 6 additions & 0 deletions mathesar/tests/integration/utils/locators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def get_tables_list(page):
return page.locator("#sidebar li[aria-level='1']:has(button:has-text('Tables')) ul")


def get_table_entry(page, table_name):
return get_tables_list(page).locator(f"li:has-text('{table_name}')")
Loading

0 comments on commit 90b4ee1

Please sign in to comment.