diff --git a/.dockerignore b/.dockerignore index 60d5509cca..84e66550e6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,5 @@ **/node_modules **/dist -**/storybook-static **/.vscode **/npm-debug.log **/Dockerfile diff --git a/.env.example b/.env.example index 7e8e2933aa..f002e016f1 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,4 @@ ALLOWED_HOSTS='.localhost, 127.0.0.1, [::1]' SECRET_KEY=2gr6ud88x=(p855_5nbj_+7^bw-iz&n7ldqv%94mjaecl+b9=4 -DJANGO_DATABASE_URL=postgres://mathesar:mathesar@mathesar_db:5432/mathesar_django -MATHESAR_DATABASES=(mathesar_tables|postgresql://mathesar:mathesar@mathesar_db:5432/mathesar) ## Uncomment the setting below to put Mathesar in 'demo mode' # DJANGO_SETTINGS_MODULE=demo.settings diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index d9516d20b1..171898e415 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ -github: centerofci +github: mathesar-foundation open_collective: mathesar diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 714a0a08f7..d7a4f0a623 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,7 @@ --- name: Bug report about: Report an issue with Mathesar -labels: "type: bug, status: triage" +labels: "type: bug, needs: triage" --- ## Description diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index d90ea9e303..1ef049c3e9 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,7 +1,7 @@ --- name: Feature request about: Suggest a new feature for Mathesar -labels: "type: enhancement, status: triage" +labels: "type: enhancement, needs: triage" --- ## Problem diff --git a/.github/actions/project_update/project_update.py b/.github/actions/project_update/project_update.py deleted file mode 100644 index df66ff61bd..0000000000 --- a/.github/actions/project_update/project_update.py +++ /dev/null @@ -1,207 +0,0 @@ -import argparse -import os -from string import Template - -from dateutil import parser -import requests - - -GITHUB_TOKEN = os.environ['MATHESAR_ORG_GITHUB_TOKEN'] - -GITHUB_ORG = 'centerofci' -MATHESAR_PROJECT_NUMBER = 1 - - -def run_graphql(graphql): - headers = { - 'Authorization': f'Bearer {GITHUB_TOKEN}', - 'GraphQL-Features': 'projects_next_graphql' - } - request = requests.post( - 'https://api.github.com/graphql', - json={'query': graphql}, - headers=headers - ) - if request.status_code == 200: - result = request.json() - print(f'\tResult of query:\n\t\t{result}') - return result - else: - raise Exception(f'\tQuery failed to run by returning code of {request.status_code}.\n\t\t{graphql}') - - -def get_project_data(): - print(f'Getting project data for project #{MATHESAR_PROJECT_NUMBER}...') - query_template = Template( - """ - { - organization(login: "$github_org") { - projectV2(number: $project_num) { - id - fields(first: 20) { - nodes { - ... on ProjectV2Field { - id - name - } - ... on ProjectV2IterationField { - id - name - configuration { - iterations { - startDate - id - } - } - } - ... on ProjectV2SingleSelectField { - id - name - options { - id - name - } - } - } - } - } - } - } - """ - ) - query = query_template.substitute(github_org=GITHUB_ORG, project_num=MATHESAR_PROJECT_NUMBER) - result = run_graphql(query) - return result['data']['organization']['projectV2'] - - -def add_item_to_project(content_id, project_data): - print(f'Adding item #{content_id} to project...') - query_template = Template( - """ - mutation { - addProjectV2ItemById(input: {projectId: "$project_id" contentId: "$content_id"}) { - item { - id - } - } - } - """ - ) - query = query_template.substitute( - project_id=project_data['id'], - content_id=content_id - ) - result = run_graphql(query) - try: - return result['data']['addProjectV2ItemById']['item']['id'] - except KeyError as e: - print(f'\tAdd item error:\n\t\t{result}') - raise e - - -def get_field_data(field_name, project_data): - fields = project_data['fields']['nodes'] - field_data = [field for field in fields if field['name'] == field_name][0] - return field_data - - -def get_option_data(option_name, field_data): - options = field_data['options'] - option_data = [option for option in options if option['name'] == option_name][0] - return option_data - - -def update_field_value_for_single_select_item(item_id, field, value, project_data): - print(f'Updating {item_id} with field ID: {field}, field value: {value}...') - query_template = Template( - """ - mutation { - updateProjectV2ItemFieldValue( - input: { - projectId: "$project_id" - itemId: "$item_id" - fieldId: "$field_id" - value: { - singleSelectOptionId: "$value" - } - } - ) { - projectV2Item { - id - } - } - } - """ - ) - field_data = get_field_data(field, project_data) - option_data = get_option_data(value, field_data) - value_to_save = option_data['id'] - query = query_template.substitute( - project_id=project_data['id'], - item_id=item_id, - field_id=field_data['id'], - value=value_to_save - ) - result = run_graphql(query) - return result - - -def update_field_value_for_text_item(item_id, field, value, project_data): - print(f'Updating {item_id} with field ID: {field}, field value: {value}...') - query_template = Template( - """ - mutation { - updateProjectV2ItemFieldValue( - input: { - projectId: "$project_id" - itemId: "$item_id" - fieldId: "$field_id" - value: { - text: "$value" - } - } - ) { - projectV2Item { - id - } - } - } - """ - ) - field_data = get_field_data(field, project_data) - value_to_save = value - query = query_template.substitute( - project_id=project_data['id'], - item_id=item_id, - field_id=field_data['id'], - value=value_to_save - ) - result = run_graphql(query) - return result - - -def get_arguments(): - parser = argparse.ArgumentParser() - parser.add_argument("content_id", help="The issue/PR number to add/update") - parser.add_argument("--status", help="Status to set ") - parser.add_argument("--priority", help="Priority to set ") - parser.add_argument("--work", help="Work to set ") - parser.add_argument("--timestamp", help="Timestamp to set ") - return parser.parse_args() - - -if __name__ == '__main__': - args = get_arguments() - content_id = args.content_id - project_data = get_project_data() - item_id = add_item_to_project(content_id, project_data) - if args.status: - update_field_value_for_single_select_item(item_id, 'Status', args.status, project_data) - if args.priority: - update_field_value_for_single_select_item(item_id, 'Priority', args.priority, project_data) - if args.work: - update_field_value_for_single_select_item(item_id, 'Work', args.work, project_data) - if args.timestamp: - timestamp = parser.parse(args.timestamp) - timestamp_str = timestamp.strftime('%Y-%m-%d %H:%M:%S') - update_field_value_for_text_item(item_id, 'Timestamp', timestamp_str, project_data) diff --git a/.github/actions/project_update/requirements.txt b/.github/actions/project_update/requirements.txt deleted file mode 100644 index bc314c3c23..0000000000 --- a/.github/actions/project_update/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -python-dateutil -requests diff --git a/.github/sync.yml b/.github/sync.yml index 98ab2a7ef6..9de822878f 100644 --- a/.github/sync.yml +++ b/.github/sync.yml @@ -1,80 +1,24 @@ -centerofci/mathesar-ansible: - - .github/actions/project_update/ - - .github/workflows/add-to-project.yml +mathesar-foundation/mathesar-ansible: - .github/workflows/toc.yml - .github/workflows/stale.yml - - .github/workflows/update-project-on-issue-close.yml - - .github/workflows/update-project-on-issue-edit.yml - - .github/workflows/update-project-on-issue-label.yml - - .github/workflows/update-project-on-pr-edit.yml - - .github/workflows/update-project-on-pr-label.yml -centerofci/mathesar-data-playground: - - .github/actions/project_update/ - - .github/workflows/add-to-project.yml +mathesar-foundation/mathesar-data-playground: - .github/workflows/toc.yml - .github/workflows/stale.yml - - .github/workflows/update-project-on-issue-close.yml - - .github/workflows/update-project-on-issue-edit.yml - - .github/workflows/update-project-on-issue-label.yml - - .github/workflows/update-project-on-pr-edit.yml - - .github/workflows/update-project-on-pr-label.yml -centerofci/mathesar-design: - - .github/actions/project_update/ - - .github/workflows/add-to-project.yml +mathesar-foundation/mathesar-design: - .github/workflows/toc.yml - .github/workflows/stale.yml - - .github/workflows/update-project-on-issue-close.yml - - .github/workflows/update-project-on-issue-edit.yml - - .github/workflows/update-project-on-issue-label.yml - - .github/workflows/update-project-on-pr-edit.yml - - .github/workflows/update-project-on-pr-label.yml -centerofci/mathesar-private-notes: - - .github/actions/project_update/ - - .github/workflows/add-to-project.yml +mathesar-foundation/mathesar-internal-crm: - .github/workflows/toc.yml - .github/workflows/stale.yml - - .github/workflows/update-project-on-issue-close.yml - - .github/workflows/update-project-on-issue-edit.yml - - .github/workflows/update-project-on-issue-label.yml - - .github/workflows/update-project-on-pr-edit.yml - - .github/workflows/update-project-on-pr-label.yml -centerofci/mathesar-scripts: - - .github/actions/project_update/ - - .github/workflows/add-to-project.yml +mathesar-foundation/mathesar-private-notes: - .github/workflows/toc.yml - .github/workflows/stale.yml - - .github/workflows/update-project-on-issue-close.yml - - .github/workflows/update-project-on-issue-edit.yml - - .github/workflows/update-project-on-issue-label.yml - - .github/workflows/update-project-on-pr-edit.yml - - .github/workflows/update-project-on-pr-label.yml -centerofci/mathesar-update-companion: - - .github/actions/project_update/ - - .github/workflows/add-to-project.yml +mathesar-foundation/mathesar-scripts: - .github/workflows/toc.yml - .github/workflows/stale.yml - - .github/workflows/update-project-on-issue-close.yml - - .github/workflows/update-project-on-issue-edit.yml - - .github/workflows/update-project-on-issue-label.yml - - .github/workflows/update-project-on-pr-edit.yml - - .github/workflows/update-project-on-pr-label.yml -centerofci/mathesar-website: - - .github/actions/project_update/ - - .github/workflows/add-to-project.yml +mathesar-foundation/mathesar-website: - .github/workflows/toc.yml - .github/workflows/stale.yml - - .github/workflows/update-project-on-issue-close.yml - - .github/workflows/update-project-on-issue-edit.yml - - .github/workflows/update-project-on-issue-label.yml - - .github/workflows/update-project-on-pr-edit.yml - - .github/workflows/update-project-on-pr-label.yml -centerofci/mathesar-wiki: - - .github/actions/project_update/ - - .github/workflows/add-to-project.yml +mathesar-foundation/mathesar-wiki: - .github/workflows/toc.yml - .github/workflows/stale.yml - - .github/workflows/update-project-on-issue-close.yml - - .github/workflows/update-project-on-issue-edit.yml - - .github/workflows/update-project-on-issue-label.yml - - .github/workflows/update-project-on-pr-edit.yml - - .github/workflows/update-project-on-pr-label.yml diff --git a/.github/workflows/add-to-project.yml b/.github/workflows/add-to-project.yml deleted file mode 100644 index 9a1914ec02..0000000000 --- a/.github/workflows/add-to-project.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Add new issues and PRs to project -on: - issues: - types: [opened] - pull_request_target: - types: [opened] - -jobs: - add_item_to_project: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.9' - - - name: Install dependencies - run: | - cd .github/actions/project_update/ - pip install -r requirements.txt - - - name: Add PR to project - if: ${{ github.event_name == 'pull_request_target' }} - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --status Started - - - name: Add issue to project - if: ${{ github.event_name == 'issues' }} - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --status Triage diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 95ca879c5d..c20fa7ad23 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: 3.x + python-version: 3.11 - run: pip install -r ./docs/requirements.txt - working-directory: ./docs run: mkdocs gh-deploy --strict --force diff --git a/.github/workflows/handle-required-checks.yml b/.github/workflows/handle-required-checks.yml deleted file mode 100644 index 40677fd4bc..0000000000 --- a/.github/workflows/handle-required-checks.yml +++ /dev/null @@ -1,22 +0,0 @@ -## "lint" and "tests" are required checks, but they run conditionally. -## This handles the cases where they don't run because no Python or JS file has been changed. -## See https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks -name: handle-required-checks -on: - push: - paths-ignore: - - 'mathesar_ui/**' - - '**.py' - pull_request: - paths-ignore: - - 'mathesar_ui/**' - - '**.py' -jobs: - lint: - runs-on: ubuntu-latest - steps: - - run: 'echo "No lint required"' - tests: - runs-on: ubuntu-latest - steps: - - run: 'echo "No tests required"' diff --git a/.github/workflows/run-flake8.yml b/.github/workflows/run-flake8.yml deleted file mode 100644 index 98eba2706c..0000000000 --- a/.github/workflows/run-flake8.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Lint Python code - -on: - push: - paths: - - '**.py' - pull_request: - paths: - - '**.py' - -jobs: - lint: - runs-on: ubuntu-latest - # We only want to run on external PRs, since internal PRs are covered by "push" - # This prevents this from running twice on internal PRs - if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - name: Run flake8 - uses: julianwachholz/flake8-action@main - with: - checkName: "flake8" - path: "." - plugins: flake8-no-types - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/run-lint-audit-tests-ui.yml b/.github/workflows/run-lint-audit-tests-ui.yml deleted file mode 100644 index 1b0d20841d..0000000000 --- a/.github/workflows/run-lint-audit-tests-ui.yml +++ /dev/null @@ -1,129 +0,0 @@ -name: UI - Lint, Audit and Tests - -on: - push: - paths: - - 'mathesar_ui/**' - pull_request: - paths: - - 'mathesar_ui/**' - -jobs: - format: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./mathesar_ui - timeout-minutes: 5 - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: 14 - - id: npm-cache-dir - run: echo "::set-output name=dir::$(npm config get cache)" - - uses: actions/cache@v2 - id: npm-cache - with: - path: ${{ steps.npm-cache-dir.outputs.dir }} - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: ${{ runner.os }}-node- - - run: npm install --no-audit --prefer-offline - - run: npm run check-format - - lint: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./mathesar_ui - timeout-minutes: 5 - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: 14 - - id: npm-cache-dir - run: echo "::set-output name=dir::$(npm config get cache)" - - uses: actions/cache@v2 - id: npm-cache - with: - path: ${{ steps.npm-cache-dir.outputs.dir }} - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: ${{ runner.os }}-node- - - run: npm install --no-audit --prefer-offline - - run: npm run lint - - typecheck: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./mathesar_ui - timeout-minutes: 5 - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: 14 - - id: npm-cache-dir - run: echo "::set-output name=dir::$(npm config get cache)" - - uses: actions/cache@v2 - id: npm-cache - with: - path: ${{ steps.npm-cache-dir.outputs.dir }} - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: ${{ runner.os }}-node- - - run: npm install --no-audit --prefer-offline - - run: npm run typecheck - - audit: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./mathesar_ui - timeout-minutes: 5 - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: 14 - - id: npm-cache-dir - run: echo "::set-output name=dir::$(npm config get cache)" - - uses: actions/cache@v2 - id: npm-cache - with: - path: ${{ steps.npm-cache-dir.outputs.dir }} - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: ${{ runner.os }}-node- - - run: npm install - - uses: oke-py/npm-audit-action@v1.8.2 - with: - audit_level: moderate - github_token: ${{ secrets.GITHUB_TOKEN }} - create_pr_comments: false - dedupe_issues: true - working_directory: './mathesar_ui' - issue_labels: 'restricted: maintainers,type: bug,work: frontend,status: triage' - production_flag: true - continue-on-error: true - - tests: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./mathesar_ui - timeout-minutes: 15 - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: 14 - - id: npm-cache-dir - run: echo "::set-output name=dir::$(npm config get cache)" - - uses: actions/cache@v2 - id: npm-cache - with: - path: ${{ steps.npm-cache-dir.outputs.dir }} - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: ${{ runner.os }}-node- - - run: npm install --no-audit --prefer-offline - - run: npm test diff --git a/.github/workflows/run-pytest.yml b/.github/workflows/run-pytest.yml deleted file mode 100644 index 170540dd75..0000000000 --- a/.github/workflows/run-pytest.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Run Python tests -on: - push: - paths: - - '*.py' - - 'mathesar/**' - - 'db/**' - pull_request: - paths: - - '*.py' - - 'mathesar/**' - - 'db/**' - - -jobs: - tests: - runs-on: ubuntu-latest - # We only want to run on external PRs, since internal PRs are covered by "push" - # This prevents this from running twice on internal PRs - if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository - steps: - - uses: actions/checkout@v2 - - - name: Copy env file - run: cp .env.example .env - - # The code is checked out under uid 1001 - reset this to 1000 for the - # container to run tests successfully - - name: Fix permissions - run: sudo chown -R 1000:1000 . - - - name: Build the stack - run: docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build -d test-service - - - name: Run tests with pytest - run: docker exec mathesar_service_test ./run_pytest.sh diff --git a/.github/workflows/run-sql-tests.yml b/.github/workflows/run-sql-tests.yml deleted file mode 100644 index 539b90dd74..0000000000 --- a/.github/workflows/run-sql-tests.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Run SQL tests -on: - push: - paths: - - '**.sql' - pull_request: - paths: - - '**.sql' - - -jobs: - tests: - runs-on: ubuntu-latest - # We only want to run on external PRs, since internal PRs are covered by "push" - # This prevents this from running twice on internal PRs - if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository - steps: - - uses: actions/checkout@v2 - - - name: Copy env file - run: cp .env.example .env - - # The code is checked out under uid 1001 - reset this to 1000 for the - # container to run tests successfully - - name: Fix permissions - run: sudo chown -R 1000:1000 . - - - name: Build the test DB - run: docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build -d dev-db - - - name: Run tests with pg_prove - run: docker exec mathesar_dev_db /bin/bash /sql/run_tests.sh diff --git a/.github/workflows/run-vulture.yml b/.github/workflows/run-vulture.yml deleted file mode 100644 index b4533aedb0..0000000000 --- a/.github/workflows/run-vulture.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Find unused code - -on: - push: - paths: - - '**.py' - pull_request: - paths: - - '**.py' - -jobs: - build: - runs-on: ubuntu-latest - # We only want to run on external PRs, since internal PRs are covered by "push" - # This prevents this from running twice on internal PRs - if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - name: Install Vulture - run: pip3 install vulture - - name: Run Vulture - run: vulture . diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml index a7a3d771bb..d5d170fff3 100644 --- a/.github/workflows/staging-deploy.yml +++ b/.github/workflows/staging-deploy.yml @@ -11,7 +11,7 @@ jobs: - name: Checkout ansible repo uses: actions/checkout@v2 with: - repository: 'centerofci/mathesar-ansible' + repository: 'mathesar-foundation/mathesar-ansible' token: ${{ secrets.MATHESAR_ORG_GITHUB_TOKEN }} # Repo is private, so an access token is used # This checkout is used for getting the 'action' from the current repo - name: Checkout mathesar repo diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml.disabled similarity index 100% rename from .github/workflows/stale.yml rename to .github/workflows/stale.yml.disabled diff --git a/.github/workflows/sync-github-labels-milestones.yml b/.github/workflows/sync-github-labels-milestones.yml index 40b640eeee..4b55fba303 100644 --- a/.github/workflows/sync-github-labels-milestones.yml +++ b/.github/workflows/sync-github-labels-milestones.yml @@ -13,19 +13,19 @@ jobs: steps: - uses: actions/checkout@v2 - run: composer global require 'vanilla/github-sync' - - run: /home/runner/.composer/vendor/bin/github-sync labels -f centerofci/mathesar -t centerofci/mathesar-ansible -d - - run: /home/runner/.composer/vendor/bin/github-sync labels -f centerofci/mathesar -t centerofci/mathesar-data-playground -d - - run: /home/runner/.composer/vendor/bin/github-sync labels -f centerofci/mathesar -t centerofci/mathesar-design -d - - run: /home/runner/.composer/vendor/bin/github-sync labels -f centerofci/mathesar -t centerofci/mathesar-private-notes -d - - run: /home/runner/.composer/vendor/bin/github-sync labels -f centerofci/mathesar -t centerofci/mathesar-scripts -d - - run: /home/runner/.composer/vendor/bin/github-sync labels -f centerofci/mathesar -t centerofci/mathesar-update-companion -d - - run: /home/runner/.composer/vendor/bin/github-sync labels -f centerofci/mathesar -t centerofci/mathesar-website -d - - run: /home/runner/.composer/vendor/bin/github-sync labels -f centerofci/mathesar -t centerofci/mathesar-wiki -d - - run: /home/runner/.composer/vendor/bin/github-sync milestones -f centerofci/mathesar -t centerofci/mathesar-ansible -s open - - run: /home/runner/.composer/vendor/bin/github-sync milestones -f centerofci/mathesar -t centerofci/mathesar-data-playground -s open - - run: /home/runner/.composer/vendor/bin/github-sync milestones -f centerofci/mathesar -t centerofci/mathesar-design -s open - - run: /home/runner/.composer/vendor/bin/github-sync milestones -f centerofci/mathesar -t centerofci/mathesar-private-notes -s open - - run: /home/runner/.composer/vendor/bin/github-sync milestones -f centerofci/mathesar -t centerofci/mathesar-scripts -s open - - run: /home/runner/.composer/vendor/bin/github-sync milestones -f centerofci/mathesar -t centerofci/mathesar-update-companion -s open - - run: /home/runner/.composer/vendor/bin/github-sync milestones -f centerofci/mathesar -t centerofci/mathesar-website -s open - - run: /home/runner/.composer/vendor/bin/github-sync milestones -f centerofci/mathesar -t centerofci/mathesar-wiki -s open + - run: /home/runner/.composer/vendor/bin/github-sync labels -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-ansible -d + - run: /home/runner/.composer/vendor/bin/github-sync labels -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-data-playground -d + - run: /home/runner/.composer/vendor/bin/github-sync labels -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-design -d + - run: /home/runner/.composer/vendor/bin/github-sync labels -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-internal-crm -d + - run: /home/runner/.composer/vendor/bin/github-sync labels -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-private-notes -d + - run: /home/runner/.composer/vendor/bin/github-sync labels -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-scripts -d + - run: /home/runner/.composer/vendor/bin/github-sync labels -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-website -d + - run: /home/runner/.composer/vendor/bin/github-sync labels -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-wiki -d + - run: /home/runner/.composer/vendor/bin/github-sync milestones -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-ansible -s open + - run: /home/runner/.composer/vendor/bin/github-sync milestones -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-data-playground -s open + - run: /home/runner/.composer/vendor/bin/github-sync milestones -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-design -s open + - run: /home/runner/.composer/vendor/bin/github-sync milestones -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-internal-crm -s open + - run: /home/runner/.composer/vendor/bin/github-sync milestones -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-private-notes -s open + - run: /home/runner/.composer/vendor/bin/github-sync milestones -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-scripts -s open + - run: /home/runner/.composer/vendor/bin/github-sync milestones -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-website -s open + - run: /home/runner/.composer/vendor/bin/github-sync milestones -f mathesar-foundation/mathesar -t mathesar-foundation/mathesar-wiki -s open diff --git a/.github/workflows/sync-workflows.yml b/.github/workflows/sync-workflows.yml index 627f410b40..454218cf48 100644 --- a/.github/workflows/sync-workflows.yml +++ b/.github/workflows/sync-workflows.yml @@ -2,7 +2,7 @@ name: Sync GitHub workflows to other repos on: push: branches: - - master + - develop workflow_dispatch: jobs: sync_workflows: @@ -13,4 +13,4 @@ jobs: - name: Run GitHub file sync uses: BetaHuhn/repo-file-sync-action@v1 with: - GH_PAT: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} \ No newline at end of file + GH_PAT: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} diff --git a/.github/workflows/test-and-lint-code.yml b/.github/workflows/test-and-lint-code.yml new file mode 100644 index 0000000000..7d52674e87 --- /dev/null +++ b/.github/workflows/test-and-lint-code.yml @@ -0,0 +1,351 @@ +## This workflow runs all tests, audits, and linters for Mathesar's source code. +## It's set up to run for every pull request change, as well as when a pull +## request is added to the merge queue. This, combined with our branch +## protections requiring pull requests and a merge queue for `develop` and +## `master` ensures that no code is merged into those branches without +## appropriate tests having run. + +name: Test and Lint Code +on: [pull_request, merge_group] +jobs: + +################################################################################ +## FILE CHANGE CHECKERS ## +## ## +## These jobs check which files have changed so that we can call appropriate ## +## testing, auditing, and linting jobs. Jobs in this section should check for ## +## file changes that would indicate whether we need to run some particular ## +## test suite, then they should output 'true' if such file changes are found. ## +## ## +################################################################################ + + python_tests_required: + name: Check for file changes requiring python tests + runs-on: ubuntu-latest + outputs: + tests_should_run: ${{ steps.changed_files.outputs.any_changed }} + steps: + - uses: actions/checkout@v4 + - name: Get changed files + id: changed_files + uses: tj-actions/changed-files@v41 + with: + files: | + *.py + mathesar/** + db/** + + python_lint_required: + name: Check for file changes requiring python linter + runs-on: ubuntu-latest + outputs: + lint_should_run: ${{ steps.changed_files.outputs.any_changed }} + steps: + - uses: actions/checkout@v4 + - name: Get changed files + id: changed_files + uses: tj-actions/changed-files@v41 + with: + files: '**.py' + + sql_tests_required: + name: Check for file changes requiring SQL tests + runs-on: ubuntu-latest + outputs: + tests_should_run: ${{ steps.changed_files.outputs.any_changed }} + steps: + - uses: actions/checkout@v4 + - name: echo + run: echo "${{needs.all_tests_required.outputs.tests_should_run}}" + - name: Get changed files + id: changed_files + uses: tj-actions/changed-files@v41 + with: + files: '**.sql' + + front_end_checks_required: + name: Check for file changes requiring front end checks + runs-on: ubuntu-latest + outputs: + checks_should_run: ${{ steps.changed_files.outputs.any_changed }} + steps: + - uses: actions/checkout@v4 + - name: Get changed files + id: changed_files + uses: tj-actions/changed-files@v41 + with: + files: 'mathesar_ui/**' + + all_be_tests_required: + name: Check for file changes requiring all backend tests + runs-on: ubuntu-latest + outputs: + tests_should_run: ${{ steps.changed_files.outputs.any_changed }} + steps: + - uses: actions/checkout@v4 + - name: Get changed files + id: changed_files + uses: tj-actions/changed-files@v41 + with: + files: | + **.yml + **.sh + Dockerfile* + +################################################################################ +## BACK END TEST/LINT RUNNERS ## +## ## +## These jobs run tests and linters. Each job in this section should be ## +## dependent on one of the FILE CHANGE CHECKERS above, and should only run if ## +## appropriate files have changed. You can see this by using a `needs:` block ## +## to make the job dependent on the relevant file checker, and an `if:` block ## +## to ensure that the file checker returned 'true' before running the actual ## +## job. Job IDs in this section must be namespaced (so `python_tests` ## +## instead of just `tests`). ## +## ## +################################################################################ + + python_tests: + name: Run Python tests + runs-on: ubuntu-latest + needs: [python_tests_required, all_be_tests_required] + if: needs.python_tests_required.outputs.tests_should_run == 'true' || + needs.all_be_tests_required.outputs.tests_should_run + strategy: + matrix: + pg-version: [13, 14, 15] + steps: + - uses: actions/checkout@v4 + - name: Copy env file + run: cp .env.example .env + # The code is checked out under uid 1001 - reset this to 1000 for the + # container to run tests successfully + - name: Fix permissions + run: sudo chown -R 1000:1000 . + - name: Build the stack + run: docker compose -f docker-compose.dev.yml up --build -d test-service + env: + PG_VERSION: ${{ matrix.pg-version }} + - name: Run tests with pytest + run: docker exec mathesar_service_test ./run_pytest.sh + + sql_tests: + name: Run SQL tests + runs-on: ubuntu-latest + needs: [sql_tests_required, all_be_tests_required] + if: needs.sql_tests_required.outputs.tests_should_run == 'true' || + needs.all_be_tests_required.outputs.tests_should_run + strategy: + matrix: + pg-version: [13, 14, 15] + steps: + - uses: actions/checkout@v4 + - name: Copy env file + run: cp .env.example .env + # The code is checked out under uid 1001 - reset this to 1000 for the + # container to run tests successfully + - name: Fix permissions + run: sudo chown -R 1000:1000 . + - name: Build the test DB + run: docker compose -f docker-compose.dev.yml up --build -d dev-db + env: + PG_VERSION: ${{ matrix.pg-version }} + - name: Run tests with pg_prove + run: docker exec mathesar_dev_db /bin/bash /sql/run_tests.sh + + python_lint: + name: Run Python linter + runs-on: ubuntu-latest + needs: [python_lint_required, all_be_tests_required] + if: needs.python_lint_required.outputs.lint_should_run == 'true' || + needs.all_be_tests_required.outputs.tests_should_run + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + - name: Run flake8 + uses: julianwachholz/flake8-action@main + with: + checkName: "flake8" + path: "." + plugins: flake8-no-types + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + vulture: + name: Find unused code + runs-on: ubuntu-latest + needs: [python_lint_required, all_be_tests_required] + if: needs.python_lint_required.outputs.lint_should_run == 'true' || + needs.all_be_tests_required.outputs.tests_should_run + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + - name: Install Vulture + run: pip3 install vulture + - name: Run Vulture + run: vulture . + +################################################################################ +## FRONT END CHECK RUNNERS ## +## ## +## These jobs run front end checks. Each job in this section should be ## +## dependent on one of the FILE CHANGE CHECKERS above (currently only ## +## front_end_checks_required), and should only run if appropriate files have ## +## changed. You can see this by using a `needs:` block to make the job ## +## dependent on the relevant file checker, and an `if:` block to ensure that ## +## the file checker returned 'true' before running the actual job. `lint` and ## +## `tests` Job IDs in this section must be namespaced (`front_end_tests` ## +## rather than `tests`). ## +## ## +################################################################################ + + front_end_format: + name: Check front end code format + runs-on: ubuntu-latest + needs: front_end_checks_required + if: needs.front_end_checks_required.outputs.checks_should_run == 'true' + defaults: + run: + working-directory: ./mathesar_ui + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - id: npm-cache-dir + run: echo "::set-output name=dir::$(npm config get cache)" + - uses: actions/cache@v3 + id: npm-cache + with: + path: ${{ steps.npm-cache-dir.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: ${{ runner.os }}-node- + - run: npm ci --no-audit --prefer-offline + - run: npm run check-format + + front_end_lint: + name: Run front end linter + runs-on: ubuntu-latest + needs: front_end_checks_required + if: needs.front_end_checks_required.outputs.checks_should_run == 'true' + defaults: + run: + working-directory: ./mathesar_ui + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - id: npm-cache-dir + run: echo "::set-output name=dir::$(npm config get cache)" + - uses: actions/cache@v3 + id: npm-cache + with: + path: ${{ steps.npm-cache-dir.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: ${{ runner.os }}-node- + - run: npm ci --no-audit --prefer-offline + - run: npm run lint + + front_end_typecheck: + name: Check front end types + runs-on: ubuntu-latest + needs: front_end_checks_required + if: needs.front_end_checks_required.outputs.checks_should_run == 'true' + defaults: + run: + working-directory: ./mathesar_ui + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - id: npm-cache-dir + run: echo "::set-output name=dir::$(npm config get cache)" + - uses: actions/cache@v3 + id: npm-cache + with: + path: ${{ steps.npm-cache-dir.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: ${{ runner.os }}-node- + - run: npm ci --no-audit --prefer-offline + - run: npm run typecheck + + front_end_audit: + name: Audit front end code + runs-on: ubuntu-latest + needs: front_end_checks_required + if: needs.front_end_checks_required.outputs.checks_should_run == 'true' + defaults: + run: + working-directory: ./mathesar_ui + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - id: npm-cache-dir + run: echo "::set-output name=dir::$(npm config get cache)" + - uses: actions/cache@v3 + id: npm-cache + with: + path: ${{ steps.npm-cache-dir.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: ${{ runner.os }}-node- + - run: npm ci + - uses: oke-py/npm-audit-action@v1.8.2 + with: + audit_level: moderate + github_token: ${{ secrets.GITHUB_TOKEN }} + create_pr_comments: false + dedupe_issues: true + working_directory: './mathesar_ui' + issue_labels: 'restricted: maintainers,type: bug,work: frontend,needs: triage' + production_flag: true + continue-on-error: true + + front_end_tests: + name: Run front end tests + runs-on: ubuntu-latest + needs: front_end_checks_required + if: needs.front_end_checks_required.outputs.checks_should_run == 'true' + defaults: + run: + working-directory: ./mathesar_ui + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - id: npm-cache-dir + run: echo "::set-output name=dir::$(npm config get cache)" + - uses: actions/cache@v3 + id: npm-cache + with: + path: ${{ steps.npm-cache-dir.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: ${{ runner.os }}-node- + - run: npm ci --no-audit --prefer-offline + - run: npm test + +################################################################################ +## REQUIRED TEST/LINT COLLECTORS ## +## Jobs in this section collect outputs from matrix-strategy testing jobs, ## +## since these are otherwise impossible to capture for branch protection ## +## purposes. At the moment, they only need to have each required check as a ## +## dependency. Required checks should skip themselves if no relevant files ## +## have changed. ## +## ## +################################################################################ + + matrix_tests: + runs-on: ubuntu-latest + needs: [python_tests, sql_tests] + steps: + - name: Report success + run: echo "All tests succeeded or skipped!" diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index 9356c734a6..ec56313c57 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: 3.x + python-version: 3.11 - run: pip install -r ./docs/requirements.txt - working-directory: ./docs run: mkdocs build --strict diff --git a/.github/workflows/update-project-on-issue-close.yml b/.github/workflows/update-project-on-issue-close.yml deleted file mode 100644 index 150ff8e71f..0000000000 --- a/.github/workflows/update-project-on-issue-close.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Update project on issue closed -on: - issues: - types: [closed] - -jobs: - update_project_on_issue_close: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.9' - - - name: Install dependencies - run: | - cd .github/actions/project_update/ - pip install -r requirements.txt - - - name: Update status and priority - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --status Done - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} diff --git a/.github/workflows/update-project-on-issue-edit.yml b/.github/workflows/update-project-on-issue-edit.yml deleted file mode 100644 index f52fbfcaa6..0000000000 --- a/.github/workflows/update-project-on-issue-edit.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Update project on issue edit -on: [issues, issue_comment] - -jobs: - update_project_on_issue_edit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.9' - - - name: Install dependencies - run: | - cd .github/actions/project_update/ - pip install -r requirements.txt - - - name: Update timestamp - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --timestamp ${{ github.event.issue.updated_at }} - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} diff --git a/.github/workflows/update-project-on-issue-label.yml b/.github/workflows/update-project-on-issue-label.yml deleted file mode 100644 index 2ce2dea128..0000000000 --- a/.github/workflows/update-project-on-issue-label.yml +++ /dev/null @@ -1,156 +0,0 @@ -name: Update project on issue label change -on: - issues: - types: [opened, labeled] - -jobs: - update_project_on_issue_label_change: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.9' - - - name: Install dependencies - run: | - cd .github/actions/project_update/ - pip install -r requirements.txt - - - name: Update Triage issues - if: "${{ contains(github.event.issue.labels.*.name, 'status: triage') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --status Triage - - - name: Update Draft issues - if: "${{ contains(github.event.issue.labels.*.name, 'status: draft') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --status Draft - - - name: Update Blocked issues - if: "${{ contains(github.event.issue.labels.*.name, 'status: blocked') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --status Blocked - - - name: Update Ready issues - if: "${{ contains(github.event.issue.labels.*.name, 'status: ready') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --status Ready - - - name: Update Started issues - if: "${{ contains(github.event.issue.labels.*.name, 'status: started') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --status Started - - - name: Update Review issues - if: "${{ contains(github.event.issue.labels.*.name, 'status: review') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --status Review - - - name: Update Waiting issues - if: "${{ contains(github.event.issue.labels.*.name, 'status: waiting') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --status Waiting - - - name: Update Done issues - if: "${{ contains(github.event.issue.labels.*.name, 'status: done') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --status Done - - - name: Update Future issues - if: "${{ contains(github.event.issue.labels.*.name, 'priority: future') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --status Draft - - - name: Update Urgent issues - if: "${{ contains(github.event.issue.labels.*.name, 'priority: urgent') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --priority Urgent - - - name: Update documentation issues - if: "${{ contains(github.event.issue.labels.*.name, 'work: documentation') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --work Documentation - - - name: Update infrastructure issues - if: "${{ contains(github.event.issue.labels.*.name, 'work: infrastructure') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --work Infrastructure - - - name: Update product issues - if: "${{ contains(github.event.issue.labels.*.name, 'work: product') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --work Product - - - name: Update design issues - if: "${{ contains(github.event.issue.labels.*.name, 'work: design') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --work Design - - - name: Update frontend issues - if: "${{ contains(github.event.issue.labels.*.name, 'work: frontend') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --work Frontend - - - name: Update backend issues - if: "${{ contains(github.event.issue.labels.*.name, 'work: backend') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --work Backend - - - name: Update database issues - if: "${{ contains(github.event.issue.labels.*.name, 'work: database') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.issue.node_id }} --work Backend diff --git a/.github/workflows/update-project-on-pr-edit.yml b/.github/workflows/update-project-on-pr-edit.yml deleted file mode 100644 index 93433320c3..0000000000 --- a/.github/workflows/update-project-on-pr-edit.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Update project on PR edit -on: - pull_request_target: - types: [assigned, unassigned, labeled, unlabeled, opened, edited, closed, reopened, synchronize, converted_to_draft, ready_for_review, locked, unlocked, review_requested, review_request_removed, auto_merge_enabled, auto_merge_disabled] - -jobs: - update_project_on_pr_edit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.9' - - - name: Install dependencies - run: | - cd .github/actions/project_update/ - pip install -r requirements.txt - - - name: Update timestamp - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --timestamp ${{ github.event.pull_request.updated_at }} - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} diff --git a/.github/workflows/update-project-on-pr-label.yml b/.github/workflows/update-project-on-pr-label.yml deleted file mode 100644 index cf169e6240..0000000000 --- a/.github/workflows/update-project-on-pr-label.yml +++ /dev/null @@ -1,156 +0,0 @@ -name: Update project on PR label change -on: - pull_request_target: - types: [opened, labeled] - -jobs: - update_project_on_pr_label_change: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.9' - - - name: Install dependencies - run: | - cd .github/actions/project_update/ - pip install -r requirements.txt - - - name: Update Triage PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'status: triage') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --status Triage - - - name: Update Draft PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'status: draft') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --status Draft - - - name: Update Blocked PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'status: blocked') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --status Blocked - - - name: Update Waiting PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'status: waiting') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --status Waiting - - - name: Update Ready PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'status: ready') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --status Ready - - - name: Update Started PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'status: started') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --status Started - - - name: Update Review PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'status: review') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --status Review - - - name: Update Done PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'status: done') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --status Done - - - name: Update Future PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'priority: future') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --status Draft - - - name: Update Urgent PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'priority: urgent') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --priority Urgent - - - name: Update documentation PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'work: documentation') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --work Documentation - - - name: Update infrastructure PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'work: infrastructure') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --work Infrastructure - - - name: Update product PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'work: product') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --work Product - - - name: Update design PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'work: design') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --work Design - - - name: Update frontend PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'work: frontend') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --work Frontend - - - name: Update backend PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'work: backend') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --work Backend - - - name: Update database PRs - if: "${{ contains(github.event.pull_request.labels.*.name, 'work: database') }}" - env: - MATHESAR_ORG_GITHUB_TOKEN: ${{secrets.MATHESAR_ORG_GITHUB_TOKEN}} - run: | - cd .github/actions/project_update/ - python project_update.py ${{ github.event.pull_request.node_id }} --work Backend diff --git a/.gitignore b/.gitignore index 71c15417fb..0bcec7040d 100644 --- a/.gitignore +++ b/.gitignore @@ -183,7 +183,6 @@ docs/_build/ # UI node_modules/ -storybook-static/ # Client Build files mathesar/static/mathesar/ @@ -196,3 +195,6 @@ core # non tracked settings config/settings/local.py + +# Transifex binary +tx diff --git a/.tx/config b/.tx/config new file mode 100755 index 0000000000..1eb356cdc6 --- /dev/null +++ b/.tx/config @@ -0,0 +1,18 @@ +[main] +host = https://app.transifex.com + +[o:mathesar:p:mathesar:r:django] +file_filter = translations//LC_MESSAGES/django.po +source_file = translations/en/LC_MESSAGES/django.po +source_lang = en +type = PO +resource_name = django +replace_edited_strings = false + +[o:mathesar:p:mathesar:r:svelte] +file_filter = mathesar_ui/src/i18n/languages//dict.json +source_file = mathesar_ui/src/i18n/languages/en/dict.json +source_lang = en +type = KEYVALUEJSON +resource_name = svelte +replace_edited_strings = false diff --git a/.tx/integration.yml b/.tx/integration.yml new file mode 100644 index 0000000000..f8b043613b --- /dev/null +++ b/.tx/integration.yml @@ -0,0 +1,14 @@ +git: + filters: + - filter_type: dir + file_format: PO + source_file_extension: po + source_language: en + source_file_dir: translations/en/LC_MESSAGES/ + translation_files_expression: 'translations//LC_MESSAGES/' + - filter_type: dir + file_format: KEYVALUEJSON + source_file_extension: json + source_language: en + source_file_dir: mathesar_ui/src/i18n/languages/en/ + translation_files_expression: 'mathesar_ui/src/i18n/languages//' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1e543ea7d6..61974ff46d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,8 @@ # Contributor Guide -Mathesar's development happens on [GitHub](https://github.com/centerofci/mathesar). We welcome contributions of all kinds! +This guide explains Mathesar's collaboration workflow and processes. Also see our [Developer Guide](./DEVELOPER_GUIDE.md) to learn how to work with Mathesar's code. + +Mathesar's development happens on [GitHub](https://github.com/mathesar-foundation/mathesar). We welcome contributions of all kinds! ## Joining the Community @@ -12,17 +14,14 @@ We highly recommend joining our [Matrix community](https://wiki.mathesar.org/en/ Make sure to **do this before moving on**. If you need help, ask in [Matrix](https://wiki.mathesar.org/en/community/matrix), taking care to form *specific* questions that people can answer asynchronously. -1. **Find an [issue](https://github.com/centerofci/mathesar/issues) to work on.** +1. **Find an [issue](https://github.com/mathesar-foundation/mathesar/issues) to work on.** - - ✅ Our easiest issues are labeled [good first issue](https://github.com/centerofci/mathesar/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+label%3A%22good+first+issue%22) and are a great place to start. However keep in mind that we're not always entirely sure of the necessary steps to solve a problem when we open an issue. - - ✅ Slightly more challenging issues are still labeled [help wanted](https://github.com/centerofci/mathesar/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+label%3A%22help+wanted%22). These can be a good place to start if you have some experience coding but are not yet familiar with our codebase. - - ❌ Issues are not appropriate if they meet any of the following criteria: - - already assigned to someone - - labeled with a `restricted: ...` label - - labeled with any `status: ...` label other than `status: ready` - - ⚠️ Some issues fall into a middle ground, not being labeled "help wanted" or "restricted". These tickets are more challenging and are only appropriate for community contributors who are familiar with our codebase. + - ✅ All issues open to community contribution are labeled [help wanted](https://github.com/mathesar-foundation/mathesar/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+label%3A%22help+wanted%22). Look through these to find a task. + - ✅ Additionally, the easiest of those issues are labeled [good first issue](https://github.com/mathesar-foundation/mathesar/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee+label%3A%22good+first+issue%22). However keep in mind that we're not always entirely sure of the necessary steps to solve a problem when we open an issue, so there could be larger challenges lurking within some of that work. + - ❌ If an issue is _not_ labeled "help wanted", then it is not open to community contribution. One of the Mathesar maintainers will work on it instead. + - ❌ Issues already assigned to other users are also not open to contribution. - If you want to work on something for which there is no GitHub issue open yet, create an issue and propose your change there. A Mathesar [team member](https://wiki.mathesar.org/en/team) will evaluate your issue and decide whether we'll accept a pull request for the issue. + If you want to work on something for which there is no GitHub issue open yet, [create an issue](https://github.com/mathesar-foundation/mathesar/issues/new/choose) and propose your change there. A Mathesar [team member](https://wiki.mathesar.org/en/team) will evaluate your issue and decide whether we'll accept a pull request for the issue. 1. ***(Optionally)* Claim the issue.** diff --git a/Caddyfile b/Caddyfile index c37041dd72..ece8bcbed1 100644 --- a/Caddyfile +++ b/Caddyfile @@ -12,33 +12,14 @@ file_server { precompressed br zstd gzip - root {$MEDIA_ROOT:/mathesar/media/} + root {$MEDIA_ROOT:/code/media/} } } handle_path /static/* { file_server { precompressed br zstd gzip - root {$STATIC_ROOT:/mathesar/static/} + root {$STATIC_ROOT:/code/static/} } } - # Rewrite and reverse proxy upgrade endpoint calls to Watchtower; - # Accepts only POST requests, rewrites them to GET. - @upgrade_request { - path /api/ui/v0/upgrade/ - method POST - } - handle @upgrade_request { - rewrite * /v1/update - method * GET - reverse_proxy watchtower:8080 { - header_up Authorization "Bearer mytoken" - transport http { - # We want keepalive connections to stay open as long as a dockerhub pull - # might take, because Watchtower responds to the upgrade request only when - # it's finished upgrading. - keepalive 0.5h - } - } - } reverse_proxy mathesar_service:8000 } diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index e240b1db04..bbd533d188 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -1,5 +1,7 @@ # Developer Guide +This guide explains how to work with Mathesar's code. Be sure to also see our [Contributor Guide](./CONTRIBUTING.md) to learn about our collaboration workflow. + ## Stack Mathesar is built using: @@ -39,7 +41,7 @@ Mathesar is built using: - username: `admin` - password: `password` -1. Keep Docker running while making your code changes. The app will update automatically with your new code. +1. Keep Docker running while making your code changes. The app will update automatically with your new code. Please refer to our [Troubleshooting guide](#troubleshooting) if you are experiencing any issues. ## Contribution guidelines @@ -121,6 +123,98 @@ Sometimes you may need to rebuild your Docker images after pulling new code chan docker compose -f docker-compose.yml -f docker-compose.dev.yml up dev-service --force-recreate --build dev-service ``` +## Internationalization + +Our repo contains two separate i18n flows: one for the server-rendered UI from **Django**, and another for the client-rendered UI handled by **Svelte**. + +### Django i18n + +We use the i18n features provided by Django. Refer the [Django docs](https://docs.djangoproject.com/en/4.2/topics/i18n/translation/#internationalization-in-template-code) on how to translate strings. + +#### When modifying UI strings + +If you make code changes to the UI strings in Django templates, follow these steps to ensure your changes are properly translated. + +1. Regenerate the English-language [django.po](./translations/en/LC_MESSAGES/django.po) file: + + ``` + docker exec mathesar_service_dev python manage.py makemessages -l en -i "mathesar_ui" -i "docs" + ``` + + > **Note:** + > + > Only generate the `.po` file for _English_. Do not update other languages using `makemessages`. They will be pulled from our translation service provider when the translation process is complete. + +1. Commit the changes to `django.po` along with your code changes. + +#### When preparing a release + +Django uses gettext, which require the `.po` files to be compiled into a more efficient form before using in production. + +1. Compile the Django messages: + + ``` + docker exec mathesar_service_dev python manage.py compilemessages + ``` + + This will produce files with `.mo` extension for each of the `.po` files. + +1. Test the app locally with different languages. + +### Svelte i18n + +- We use [svelte-i18n](https://github.com/kaisermann/svelte-i18n), which internally uses [format-js](https://formatjs.io/) for handling i18n. +- The source translation file is [en/dict.json](./mathesar_ui/src/i18n/languages/en/dict.json). +- To handle pluralization and other complexities, the source translation strings may utilize a special syntax called [JSON with ICU Plurals](https://help.transifex.com/en/articles/6220806-json-with-icu-plurals) (a subset of the [ICU format](https://unicode-org.github.io/icu/userguide/icu/i18n.html)). +- After making changes to your code, ensure that the source `/en/dict.json` file contains new translation strings, if any. +- Do not update other translation files. They will be pulled from our translation service provider when the translation process is complete. + +## Translation process + +- We use [Transifex](https://app.transifex.com/mathesar/mathesar/dashboard/) for managing our translation process. +- You'll need to be a member of the Mathesar organization in Transifex, inorder to work with translations. Please reach out to us for information on how to join. + +### For Translators + +_(We're currently working on a workflow for translators. This section will be updated once we have a clear set of instructions to follow.)_ + +### For Maintainers + +#### Automation + +- We have automated sync between Transifex and the `develop` branch, via the GitHub integration feature provided by Transifex. +- The configuration for it is specified in the `.tx/integration.yml` file, and within the Transifex admin panel. +- Refer [Transfiex documentation](https://help.transifex.com/en/articles/6265125-github-installation-and-configuration) for more information. + +#### Manually pushing and pulling translations + +If you'd like to manually push or pull translations, follow the instructions in this section. + +> **Warning** +> +> Only push and pull translations on the `develop` branch. Do not do it for other branches since this will overwrite the existing resources within Transifex. + +1. Install the Transifex cli tool, `tx`, if you haven't already. + + ``` + curl -o- https://raw.githubusercontent.com/transifex/cli/master/install.sh | bash + ``` + + It can be installed in your host machine or on the docker container. + +1. **Push** the updated source translation files: + + ``` + TX_TOKEN= tx push -s + ``` + +1. **Pull** the translations from Transifex: + + ``` + TX_TOKEN= tx pull -f + ``` + +1. Commit and push the changes to our repo. ## Demo mode @@ -140,9 +234,40 @@ See our [Live demo mode](./demo/README.md) guide for more information on enablin - To open a PostgreSQL [psql](https://www.postgresql.org/docs/current/app-psql.html) terminal for the data in Mathesar: ``` - docker exec -it mathesar_db psql -U mathesar + docker exec -it mathesar_dev_db psql -U mathesar + ``` + + +## Building Debian package + +- On a Debian machine, install the following dependencies + + ``` + sudo apt install debhelper-compat dh-virtualenv libsystemd-dev libpq-dev libicu-dev pkg-config lsb-release python3-dev python3 python3-setuptools python3-pip python3-venv tar ``` +- Setup Mathesar build environment. + This step is useful only when testing locally is needed for building static files and for collecting them. We won't have a need for this step while using the build service as it will be using the source code from release assets which will contain these static files + +- Install Python and Nodejs preferably on a Linux machine +- Run the following commands to set up the environment + + ``` + python3 -m venv ./mathesar-venv + source ./mathesar-venv/bin/activate + pip install -r requirements.txt + sudo npm install -g npm-force-resolutions + cd mathesar_ui && npm install --unsafe-perm && npm run build + cd .. + python manage.py collectstatic + ``` + +- From the mathesar directory, run the build script to generate the debian package + + ``` + cd release-scripts && source build-debian.sh + ``` + ## Troubleshooting ### Permissions within Windows diff --git a/Dockerfile b/Dockerfile index b8b6ac4963..20f7c6d16a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,26 +2,79 @@ FROM python:3.9-buster ARG PYTHON_REQUIREMENTS=requirements.txt ENV PYTHONUNBUFFERED=1 ENV DOCKERIZE_VERSION v0.6.1 +ENV NODE_MAJOR 18 +ARG BUILD_PG_MAJOR=15 +ENV PG_MAJOR=$BUILD_PG_MAJOR -# Install dockerize -RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ - && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ - && rm dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz +RUN set -eux; -# Install node -RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - -RUN apt-get update -RUN apt install -y sudo nodejs && rm -rf /var/lib/apt/lists/* +#---------- 1. INSTALL SYSTEM DEPENDENCIES -----------------------------------# + +RUN mkdir -p /etc/apt/keyrings; + +# Add Postgres source +RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - ; \ + echo "deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main" > /etc/apt/sources.list.d/pgdg.list; + +# Add Node.js source +RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \ + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; + +# Install common dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + sudo \ + ca-certificates \ + curl \ + gnupg \ + gettext \ + nodejs \ + locales \ + && rm -rf /var/lib/apt/lists/* + +# Define Locale +RUN localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 +ENV LANG en_US.utf8 + +# Install Postgres +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + postgresql-$PG_MAJOR postgresql-client-$PG_MAJOR postgresql-contrib-$PG_MAJOR \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + + +#---------- 2. CONFIGURE SYSTEM DEPENDENCIES ---------------------------------# + +# Postgres + +ENV PATH $PATH:/usr/lib/postgresql/$PG_MAJOR/bin +ENV PGDATA /var/lib/postgresql/mathesar + +VOLUME /etc/postgresql/ +VOLUME /var/lib/postgresql/ + + +# We set the default STOPSIGNAL to SIGINT, which corresponds to what PostgreSQL +# calls "Fast Shutdown mode" wherein new connections are disallowed and any +# in-progress transactions are aborted, allowing PostgreSQL to stop cleanly and +# flush tables to disk, which is the best compromise available to avoid data +# corruption. + +STOPSIGNAL SIGINT + +EXPOSE 5432 + + +#---------- 3. SETUP MATHESAR ------------------------------------------------# -# Change work directory WORKDIR /code/ -# Copy all the requirements COPY requirements* ./ -RUN pip install --no-cache-dir -r ${PYTHON_REQUIREMENTS} --force-reinstall sqlalchemy-filters +RUN pip install --no-cache-dir -r ${PYTHON_REQUIREMENTS} COPY . . -RUN sudo npm install -g npm-force-resolutions -RUN cd mathesar_ui && npm install --unsafe-perm && npm run build +RUN cd mathesar_ui && npm ci && npm run build + EXPOSE 8000 3000 6006 + ENTRYPOINT ["./run.sh"] diff --git a/Dockerfile.devdb b/Dockerfile.devdb index baa1cc7ac2..8cdffb9560 100644 --- a/Dockerfile.devdb +++ b/Dockerfile.devdb @@ -1,4 +1,6 @@ -FROM postgres:13 +ARG PG_VERSION=13 +FROM postgres:${PG_VERSION} +ARG PG_VERSION RUN apt update -RUN apt install -y postgresql-13-pgtap postgresql-13-pldebugger && rm -rf /var/lib/apt/lists/* +RUN apt install -y postgresql-${PG_VERSION}-pgtap postgresql-${PG_VERSION}-pldebugger && rm -rf /var/lib/apt/lists/* diff --git a/Dockerfile.integ-tests b/Dockerfile.integ-tests index ea6dd2c440..ad04a97416 100644 --- a/Dockerfile.integ-tests +++ b/Dockerfile.integ-tests @@ -13,8 +13,17 @@ ENV PATH /usr/local/bin:$PATH # > At the moment, setting "LANG=C" on a Linux system *fundamentally breaks Python 3*, and that's not OK. ENV LANG C.UTF-8 -# Download node14 source -RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - +# Download node source +ENV NODE_MAJOR 18 + +RUN apt-get update +RUN apt-get install -y ca-certificates curl gnupg +RUN mkdir -p /etc/apt/keyrings +RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg +RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list + +RUN apt-get update +RUN apt-get install nodejs -y # extra dependencies (over what buildpack-deps already includes) RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -122,12 +131,11 @@ COPY requirements.txt . COPY requirements-dev.txt . COPY requirements-demo.txt . -RUN pip install -r requirements.txt --force-reinstall sqlalchemy-filters +RUN pip install -r requirements.txt RUN pip install -r requirements-dev.txt RUN pip install -r requirements-demo.txt COPY . . -RUN sudo npm install -g npm-force-resolutions -RUN cd mathesar_ui && npm install --unsafe-perm && npm run build +RUN cd mathesar_ui && npm ci && npm run build EXPOSE 8000 3000 6006 diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000000..cf8c9939f0 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include static * \ No newline at end of file diff --git a/README.md b/README.md index 9c5c698d94..7344f65aea 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@

An intuitive UI for managing data, for users of all technical skill levels. Built on Postgres.

- License - GitHub closed issues - GitHub commit activity - Codecov + License + GitHub closed issues + GitHub commit activity + Codecov

@@ -39,7 +39,7 @@ You can use Mathesar to build **data models**, **enter data**, and even **build ## Sponsors -Our top sponsors! Become a sponsor on [GitHub](https://github.com/sponsors/centerofci) or [Open Collective](https://opencollective.com/mathesar). +Our top sponsors! Become a sponsor on [GitHub](https://github.com/sponsors/mathesar-foundation) or [Open Collective](https://opencollective.com/mathesar). diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index dcfe9cabf1..8c21d9151f 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -2,6 +2,6 @@ If you run into problems, here's what you can try: 1. Narrow down the problem you're having to a minimal search phrase -2. Search on [our issue tracker](https://github.com/centerofci/mathesar/issues/) -3. If you think this is a previously unreported problem, report on [our issue tracker](https://github.com/centerofci/mathesar/issues/) +2. Search on [our issue tracker](https://github.com/mathesar-foundation/mathesar/issues/) +3. If you think this is a previously unreported problem, report on [our issue tracker](https://github.com/mathesar-foundation/mathesar/issues/) 4. In case you're having trouble formulating a report, reach out [on Matrix](https://wiki.mathesar.org/en/community/matrix) diff --git a/config/context_processors.py b/config/context_processors.py index da5e98a752..e4d87670df 100644 --- a/config/context_processors.py +++ b/config/context_processors.py @@ -1,17 +1,65 @@ from django.conf import settings +from django.templatetags.static import static + from mathesar.utils.frontend import get_manifest_data def frontend_settings(request): + manifest_data = get_manifest_data() + development_mode = settings.MATHESAR_MODE == 'DEVELOPMENT' + + i18n_settings = get_i18n_settings(manifest_data, development_mode) frontend_settings = { - 'development_mode': settings.MATHESAR_MODE == 'DEVELOPMENT', - 'manifest_data': get_manifest_data(), + 'development_mode': development_mode, + 'manifest_data': manifest_data, 'live_demo_mode': getattr(settings, 'MATHESAR_LIVE_DEMO', False), 'live_demo_username': getattr(settings, 'MATHESAR_LIVE_DEMO_USERNAME', None), 'live_demo_password': getattr(settings, 'MATHESAR_LIVE_DEMO_PASSWORD', None), + **i18n_settings } # Only include development URL if we're in development mode. if frontend_settings['development_mode'] is True: frontend_settings['client_dev_url'] = settings.MATHESAR_CLIENT_DEV_URL + return frontend_settings + + +def get_display_language_from_request(request): + # https://docs.djangoproject.com/en/4.2/topics/i18n/translation/#how-django-discovers-language-preference + # This automatically fallbacks to en because of https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-LANGUAGE_CODE + lang_from_locale_middleware = request.LANGUAGE_CODE + + if request.user.is_authenticated: + return request.user.display_language or lang_from_locale_middleware + else: + return lang_from_locale_middleware + + +def get_i18n_settings(manifest_data, development_mode): + """ + Hard coding this for now + but will be taken from users model + and cookies later on + """ + display_language = 'en' + fallback_language = 'en' + + client_dev_url = settings.MATHESAR_CLIENT_DEV_URL + + if development_mode is True: + module_translations_file_path = f'{client_dev_url}/src/i18n/languages/{display_language}/index.ts' + legacy_translations_file_path = f'{client_dev_url}/src/i18n/languages/{display_language}/index.ts' + else: + try: + module_translations_file_path = static(manifest_data[display_language]["file"]) + legacy_translations_file_path = static(manifest_data[f"{display_language}-legacy"]["file"]) + except KeyError: + module_translations_file_path = static(manifest_data[fallback_language]["file"]) + legacy_translations_file_path = static(manifest_data[f"{fallback_language}-legacy"]["file"]) + + return { + 'module_translations_file_path': module_translations_file_path, + 'legacy_translations_file_path': legacy_translations_file_path, + 'display_language': display_language + } diff --git a/config/settings/common_settings.py b/config/settings/common_settings.py index b72d22ddad..44bdb23bff 100644 --- a/config/settings/common_settings.py +++ b/config/settings/common_settings.py @@ -15,6 +15,7 @@ from decouple import Csv, config as decouple_config from dj_database_url import parse as db_url +from django.utils.translation import gettext_lazy # We use a 'tuple' with pipes as delimiters as decople naively splits the global @@ -50,6 +51,7 @@ def pipe_delim(pipe_string): "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", @@ -92,13 +94,26 @@ def pipe_delim(pipe_string): # See pipe_delim above for why we use pipes as delimiters DATABASES = { db_key: db_url(url_string) - for db_key, url_string in decouple_config('MATHESAR_DATABASES', cast=Csv(pipe_delim)) + for db_key, url_string in decouple_config('MATHESAR_DATABASES', default='', cast=Csv(pipe_delim)) } -DATABASES[decouple_config('DJANGO_DATABASE_KEY', default="default")] = decouple_config('DJANGO_DATABASE_URL', cast=db_url) + +# POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST & POSTGRES_PORT are required env variables for forming a pg connection string for the django database +# lack of any one of these will result in the internal django database to be sqlite. +POSTGRES_DB = decouple_config('POSTGRES_DB', default=None) +POSTGRES_USER = decouple_config('POSTGRES_USER', default=None) +POSTGRES_PASSWORD = decouple_config('POSTGRES_PASSWORD', default=None) +POSTGRES_HOST = decouple_config('POSTGRES_HOST', default=None) +POSTGRES_PORT = decouple_config('POSTGRES_PORT', default=None) + +if POSTGRES_DB and POSTGRES_USER and POSTGRES_PASSWORD and POSTGRES_HOST and POSTGRES_PORT: + DATABASES['default'] = db_url(f'postgres://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}') +else: + DATABASES['default'] = db_url('sqlite:///db.sqlite3') for db_key, db_dict in DATABASES.items(): - # Engine can be '.postgresql' or '.postgresql_psycopg2' - if not db_dict['ENGINE'].startswith('django.db.backends.postgresql'): + # Engine should be '.postgresql' or '.postgresql_psycopg2' for all db(s), + # however for the internal 'default' db 'sqlite3' can be used. + if not db_dict['ENGINE'].startswith('django.db.backends.postgresql') and db_key != 'default': raise ValueError( f"{db_key} is not a PostgreSQL database. " f"{db_dict['ENGINE']} found for {db_key}'s engine." @@ -113,12 +128,12 @@ def pipe_delim(pipe_string): # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = decouple_config('SECRET_KEY') +SECRET_KEY = decouple_config('SECRET_KEY', default="2gr6ud88x=(p855_5nbj_+7^gw-iz&n7ldqv%94mjaecl+b9=4") # SECURITY WARNING: don't run with debug turned on in production! DEBUG = decouple_config('DEBUG', default=False, cast=bool) -ALLOWED_HOSTS = decouple_config('ALLOWED_HOSTS', cast=Csv()) +ALLOWED_HOSTS = decouple_config('ALLOWED_HOSTS', cast=Csv(), default=".localhost, 127.0.0.1, [::1]") # Password validation # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators @@ -163,8 +178,8 @@ def pipe_delim(pipe_string): STATIC_ROOT = os.path.join(BASE_DIR, 'static/') # Media files (uploaded by the user) - -MEDIA_ROOT = os.path.join(BASE_DIR, '.media/') +DEFAULT_MEDIA_ROOT = os.path.join(BASE_DIR, '.media/') +MEDIA_ROOT = decouple_config('MEDIA_ROOT', default=DEFAULT_MEDIA_ROOT) MEDIA_URL = "/media/" @@ -178,6 +193,9 @@ def pipe_delim(pipe_string): 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ], + 'DEFAULT_PARSER_CLASSES': [ + 'rest_framework.parsers.JSONParser', + ], 'DEFAULT_FILTER_BACKENDS': ( 'django_filters.rest_framework.DjangoFilterBackend', 'rest_framework.filters.OrderingFilter', @@ -227,7 +245,10 @@ def pipe_delim(pipe_string): MATHESAR_MODE = decouple_config('MODE', default='PRODUCTION') MATHESAR_UI_BUILD_LOCATION = os.path.join(BASE_DIR, 'mathesar/static/mathesar/') MATHESAR_MANIFEST_LOCATION = os.path.join(MATHESAR_UI_BUILD_LOCATION, 'manifest.json') -MATHESAR_CLIENT_DEV_URL = 'http://localhost:3000' +MATHESAR_CLIENT_DEV_URL = decouple_config( + 'MATHESAR_CLIENT_DEV_URL', + default='http://localhost:3000' +) MATHESAR_UI_SOURCE_LOCATION = os.path.join(BASE_DIR, 'mathesar_ui/') MATHESAR_CAPTURE_UNHANDLED_EXCEPTION = decouple_config('CAPTURE_UNHANDLED_EXCEPTION', default=False) MATHESAR_STATIC_NON_CODE_FILES_LOCATION = os.path.join(BASE_DIR, 'mathesar/static/non-code/') @@ -248,3 +269,14 @@ def pipe_delim(pipe_string): } # List of Template names that contains additional script tags to be added to the base template BASE_TEMPLATE_ADDITIONAL_SCRIPT_TEMPLATES = [] + +# i18n +LANGUAGES = [ + ('en', gettext_lazy('English')), + ('ja', gettext_lazy('Japanese')), +] +LOCALE_PATHS = [ + 'translations' +] +LANGUAGE_COOKIE_NAME = 'display_language' +SALT_KEY = SECRET_KEY diff --git a/config/settings/openapi.py b/config/settings/openapi.py index 6ff2fa2f45..85a1cd0ca5 100644 --- a/config/settings/openapi.py +++ b/config/settings/openapi.py @@ -1,9 +1,16 @@ def custom_preprocessing_hook(endpoints): - filtered = [] - for (path, path_regex, method, callback) in endpoints: - # Remove all but DRF API endpoints - if path.startswith("/api/db/v0/databases/") or path.startswith("/api/db/v0/data_files/") or path.startswith("/api/db/v0/schemas/"): - filtered.append((path, path_regex, method, callback)) + prefixes = [ + "/api/db/v0/databases/", + "/api/db/v0/data_files/", + "/api/db/v0/schemas/", + "/api/db/v0/tables/", + "/api/db/v0/links/", + "/api/db/v0/queries/", + "/api/ui/v0/databases/", + "/api/ui/v0/users/", + "/api/ui/v0/database_roles/" + ] + filtered = [(path, path_regex, method, callback) for path, path_regex, method, callback in endpoints if any(path.startswith(prefix) for prefix in prefixes)] return filtered diff --git a/config/settings/production.py b/config/settings/production.py index 18a2b7734c..9af47d2268 100644 --- a/config/settings/production.py +++ b/config/settings/production.py @@ -1,8 +1,8 @@ from config.settings.common_settings import * # noqa # Override default settings - - +DEBUG = False +MATHESAR_MODE = 'PRODUCTION' # Use a local.py module for settings that shouldn't be version tracked try: from .local import * # noqa diff --git a/conftest.py b/conftest.py index 81e9a82354..41589a1e07 100644 --- a/conftest.py +++ b/conftest.py @@ -54,7 +54,7 @@ def create_db(request, SES_engine_cache): A factory for Postgres mathesar-installed databases. A fixture made of this method tears down created dbs when leaving scope. - This method is used to create two fixtures with different scopes, that's why it's not a fixture + This method is used to create fixtures with different scopes, that's why it's not a fixture itself. """ engine_cache = SES_engine_cache diff --git a/db-run.sh b/db-run.sh new file mode 100755 index 0000000000..a01bea64a7 --- /dev/null +++ b/db-run.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -e + +# Loads various settings that are used elsewhere in the script +# This should be called before any other functions +docker_setup_env() { + declare -g DATABASE_ALREADY_EXISTS + # look specifically for PG_VERSION, as it is expected in the DB dir + if [ -s "$PGDATA/PG_VERSION" ]; then + DATABASE_ALREADY_EXISTS='true' + fi +} + +docker_setup_env +# only run initialization on an empty data directory +if [ -z "$DATABASE_ALREADY_EXISTS" ]; then + pg_createcluster -d "$PGDATA" -p 5432 -u "postgres" "$PG_MAJOR" mathesar + # Create a temporary postgres server for setting password to the postgres user and for creating the default database + pg_ctlcluster "$PG_MAJOR" mathesar start + sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'mathesar';" + sudo -u postgres psql -c "CREATE DATABASE mathesar_django;" + pg_ctlcluster "$PG_MAJOR" mathesar stop +fi +pg_ctlcluster "$PG_MAJOR" mathesar start diff --git a/db/columns/defaults.py b/db/columns/defaults.py index c1deebaef8..bb5153b30c 100644 --- a/db/columns/defaults.py +++ b/db/columns/defaults.py @@ -4,6 +4,7 @@ NAME = "name" +DESCRIPTION = "description" NULLABLE = "nullable" PRIMARY_KEY = "primary_key" TYPE = "sa_type" diff --git a/db/columns/operations/alter.py b/db/columns/operations/alter.py index 95322d6e1d..04d089162d 100644 --- a/db/columns/operations/alter.py +++ b/db/columns/operations/alter.py @@ -4,7 +4,7 @@ SyntaxError ) from db import connection as db_conn -from db.columns.defaults import NAME, NULLABLE +from db.columns.defaults import NAME, NULLABLE, DESCRIPTION from db.columns.exceptions import InvalidDefaultError, InvalidTypeError, InvalidTypeOptionError @@ -25,7 +25,8 @@ def alter_column(engine, table_oid, column_attnum, column_data, connection=None) "type_options": , "column_default_dict": {"is_dynamic": , "value": } "nullable": , - "name": + "name": , + "description": } """ column_alter_def = _process_column_alter_dict(column_data, column_attnum) @@ -52,8 +53,12 @@ def alter_column(engine, table_oid, column_attnum, column_data, connection=None) engine, 'get_column_name', table_oid, column_attnum ).fetchone()[0] raise InvalidTypeError(column_db_name, requested_type) - except SyntaxError: - raise InvalidTypeOptionError + except SyntaxError as e: + # TODO this except catch is too broad; syntax errors can be caused + # by many things, especially programmer errors during development. + # find a way to be more selective about what we call an invalid + # type option error. + raise InvalidTypeOptionError(e) else: db_conn.execute_msar_func_with_psycopg2_conn( connection, 'alter_columns', @@ -186,7 +191,8 @@ def _process_column_alter_dict(column_data, column_attnum=None): "column_default_dict": {"is_dynamic": , "value": } "nullable": , "name": , - "delete": + "delete": , + "description": } Output form: @@ -195,7 +201,8 @@ def _process_column_alter_dict(column_data, column_attnum=None): "name": , "not_null": , "default": , - "delete": + "delete": , + "description": } Note that keys with empty values will be dropped, unless the given "default" @@ -218,9 +225,15 @@ def _process_column_alter_dict(column_data, column_attnum=None): "type": new_type, "not_null": column_not_null, "name": column_name, - "delete": column_delete + "delete": column_delete, } col_alter_def = {k: v for k, v in raw_col_alter_def.items() if v is not None} + # NOTE DESCRIPTION is set separately, because it shouldn't be removed if its + # value is None (that signals that the description should be removed in the + # db). + if DESCRIPTION in column_data: + column_description = column_data.get(DESCRIPTION) + col_alter_def[DESCRIPTION] = column_description default_dict = column_data.get(DEFAULT_DICT, {}) if default_dict is not None and DEFAULT_KEY in default_dict: default_value = column_data.get(DEFAULT_DICT, {}).get(DEFAULT_KEY) diff --git a/db/columns/operations/create.py b/db/columns/operations/create.py index fa07672848..7361276015 100644 --- a/db/columns/operations/create.py +++ b/db/columns/operations/create.py @@ -5,7 +5,7 @@ from alembic.operations import Operations from psycopg.errors import InvalidTextRepresentation, InvalidParameterValue -from db.columns.defaults import DEFAULT, NAME, NULLABLE, TYPE +from db.columns.defaults import DEFAULT, NAME, NULLABLE, TYPE, DESCRIPTION from db.columns.exceptions import InvalidDefaultError, InvalidTypeOptionError from db.connection import execute_msar_func_with_engine from db.tables.operations.select import reflect_table_from_oid @@ -26,12 +26,14 @@ def create_column(engine, table_oid, column_data): column_type_options = column_data.get("type_options", {}) column_nullable = column_data.get(NULLABLE, True) default_value = column_data.get(DEFAULT, {}).get('value') + column_description = column_data.get(DESCRIPTION) col_create_def = [ { "name": column_name, "type": {"name": column_type_id, "options": column_type_options}, "not_null": not column_nullable, "default": default_value, + "description": column_description, } ] try: diff --git a/db/columns/operations/select.py b/db/columns/operations/select.py index b4a3a599cd..d1b22aba6d 100644 --- a/db/columns/operations/select.py +++ b/db/columns/operations/select.py @@ -8,6 +8,13 @@ from db.utils import execute_statement, get_pg_catalog_table +def get_column_description(oid, attnum, engine): + cursor = execute_msar_func_with_engine(engine, 'col_description', oid, attnum) + row = cursor.fetchone() + description = row[0] + return description + + def get_column_attnum_from_names_as_map(table_oid, column_names, engine, metadata, connection_to_use=None): statement = _get_columns_attnum_from_names(table_oid, column_names, engine, metadata=metadata) attnums_tuple = execute_statement(engine, statement, connection_to_use).fetchall() diff --git a/db/functions/operations/__init__.py b/db/functions/operations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/db/install.py b/db/install.py index 43440b398d..fbeede2466 100644 --- a/db/install.py +++ b/db/install.py @@ -1,5 +1,6 @@ +from psycopg.errors import InsufficientPrivilege from sqlalchemy import text -from sqlalchemy.exc import OperationalError +from sqlalchemy.exc import OperationalError, ProgrammingError from db import engine from db.sql import install as sql_install @@ -7,7 +8,14 @@ def install_mathesar( - database_name, username, password, hostname, port, skip_confirm + database_name, + username, + password, + hostname, + port, + skip_confirm, + create_db=True, + root_db='postgres' ): """Create database and install Mathesar on it.""" user_db_engine = engine.create_future_engine( @@ -16,50 +24,71 @@ def install_mathesar( ) try: user_db_engine.connect() - print(f"Installing Mathesar on preexisting PostgreSQL database {database_name} at host {hostname}...") + print( + "Installing Mathesar on preexisting PostgreSQL database" + f" {database_name} at host {hostname}..." + ) sql_install.install(user_db_engine) types_install.install_mathesar_on_database(user_db_engine) user_db_engine.dispose() - except OperationalError: - database_created = _create_database( - database_name=database_name, - hostname=hostname, - username=username, - password=password, - port=port, - skip_confirm=skip_confirm - ) + except OperationalError as e: + if create_db: + database_created = _create_database( + db_name=database_name, + hostname=hostname, + username=username, + password=password, + port=port, + skip_confirm=skip_confirm, + root_database=root_db + ) + else: + database_created = False if database_created: - print(f"Installing Mathesar on PostgreSQL database {database_name} at host {hostname}...") + print( + "Installing Mathesar on PostgreSQL database" + f" {database_name} at host {hostname}..." + ) sql_install.install(user_db_engine) types_install.install_mathesar_on_database(user_db_engine) user_db_engine.dispose() else: print(f"Skipping installing on DB with key {database_name}.") + raise e -def _create_database(database_name, hostname, username, password, port, skip_confirm=True): +def _create_database( + db_name, hostname, username, password, port, skip_confirm, root_database +): if skip_confirm is True: create_database = "y" else: create_database = input( - f"Create a new Database called {database_name}? (y/n) > " + f"Create a new Database called {db_name}? (y/n) > " ) if create_database.lower() in ["y", "yes"]: - # We need to connect to an existing database inorder to create a new Database. - # So we use the default Database `postgres` that comes with postgres. - # TODO Throw correct error when the default postgres database does not exists(which is very rare but still possible) - root_database = "postgres" + # We need to connect to an existing database inorder to create a new + # Database. So we use the default database `postgres` that comes with + # postgres. + # TODO Throw correct error when the root database does not exist. root_db_engine = engine.create_future_engine( username, password, hostname, root_database, port, connect_args={"connect_timeout": 10} ) - with root_db_engine.connect() as conn: - conn.execution_options(isolation_level="AUTOCOMMIT") - conn.execute(text(f'CREATE DATABASE "{database_name}"')) - root_db_engine.dispose() - print(f"Created DB is {database_name}.") - return True + try: + with root_db_engine.connect() as conn: + conn.execution_options(isolation_level="AUTOCOMMIT") + conn.execute(text(f'CREATE DATABASE "{db_name}"')) + root_db_engine.dispose() + print(f"Created DB is {db_name}.") + return True + except ProgrammingError as e: + if isinstance(e.orig, InsufficientPrivilege): + print(f"Database {db_name} could not be created due to Insufficient Privilege") + return False + except Exception: + print(f"Database {db_name} could not be created!") + return False else: - print(f"Database {database_name} not created!") + print(f"Database {db_name} not created!") return False diff --git a/db/records/exceptions.py b/db/records/exceptions.py index d97331e4d1..346467a983 100644 --- a/db/records/exceptions.py +++ b/db/records/exceptions.py @@ -1,12 +1,8 @@ -from sqlalchemy_filters.exceptions import FieldNotFound - - -# Grouping exceptions follow the sqlalchemy_filters exceptions patterns class BadGroupFormat(Exception): pass -class GroupFieldNotFound(FieldNotFound): +class GroupFieldNotFound(Exception): pass diff --git a/db/records/operations/insert.py b/db/records/operations/insert.py index bc6ca1a26d..c61dacfebb 100644 --- a/db/records/operations/insert.py +++ b/db/records/operations/insert.py @@ -7,6 +7,7 @@ from psycopg2.errors import NotNullViolation, ForeignKeyViolation, DatatypeMismatch, UniqueViolation, ExclusionViolation from db.columns.exceptions import NotNullError, ForeignKeyError, TypeMismatchError, UniqueValueError, ExclusionError from db.columns.base import MathesarColumn +from db.constants import ID, ID_ORIGINAL from db.encoding_utils import get_sql_compatible_encoding from db.records.operations.select import get_record from sqlalchemy import select @@ -89,6 +90,8 @@ def insert_records_from_json(table, engine, json_filepath, column_names, max_lev records = get_records_from_dataframe(df) for i, row in enumerate(records): + if ID in row and ID_ORIGINAL in column_names: + row[ID_ORIGINAL] = row.pop("id") records[i] = { k: json.dumps(v) if (isinstance(v, dict) or isinstance(v, list)) diff --git a/db/records/operations/relevance.py b/db/records/operations/relevance.py index bd804495f2..056f65eaac 100644 --- a/db/records/operations/relevance.py +++ b/db/records/operations/relevance.py @@ -1,5 +1,4 @@ -from sqlalchemy import case, select -from sqlalchemy_filters import apply_sort +from sqlalchemy import case, select, desc from db.types import categories from db.types.operations.convert import get_db_type_enum_from_class @@ -18,10 +17,7 @@ def get_rank_and_filter_rows_query(relation, parameters_dict, limit=10): parameters given in parameters_dict. """ rank_cte = _get_scored_selectable(relation, parameters_dict) - filtered_ordered_cte = apply_sort( - select(rank_cte).where(rank_cte.columns[SCORE_COL] > 0), - {'field': SCORE_COL, 'direction': 'desc'} - ).cte() + filtered_ordered_cte = select(rank_cte).where(rank_cte.columns[SCORE_COL] > 0).order_by(desc(SCORE_COL)).cte() return select( *[filtered_ordered_cte.columns[c] for c in [col.name for col in relation.columns]] ).limit(limit) diff --git a/db/records/operations/select.py b/db/records/operations/select.py index ef2f91a759..4da11f90f5 100644 --- a/db/records/operations/select.py +++ b/db/records/operations/select.py @@ -42,10 +42,8 @@ def get_records( 'direction' field. search: list of dictionaries, where each dictionary has a 'column' and 'literal' field. - See: https://github.com/centerofci/sqlalchemy-filters#sort-format filter: a dictionary with one key-value pair, where the key is the filter id and the value is a list of parameters; supports composition/nesting. - See: https://github.com/centerofci/sqlalchemy-filters#filters-format group_by: group.GroupBy object duplicate_only: list of column names; only rows that have duplicates across those rows will be returned diff --git a/db/sql/0_msar.sql b/db/sql/0_msar.sql index 48f49f876b..e16bf53246 100644 --- a/db/sql/0_msar.sql +++ b/db/sql/0_msar.sql @@ -64,6 +64,7 @@ CREATE SCHEMA IF NOT EXISTS msar; ---------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- + CREATE OR REPLACE FUNCTION __msar.exec_ddl(command text) RETURNS text AS $$/* Execute the given command, returning the command executed. @@ -112,6 +113,24 @@ $$ LANGUAGE sql RETURNS NULL ON NULL INPUT; ---------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- +CREATE OR REPLACE FUNCTION msar.col_description(tab_id oid, col_id integer) RETURNS text AS $$/* +Transparent wrapper for col_description. Putting it in the `msar` namespace helps route all DB calls +from Python through a single Python module. +*/ + BEGIN + RETURN col_description(tab_id, col_id); + END +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION __msar.jsonb_key_exists(data jsonb, key text) RETURNS boolean AS $$/* +Wraps the `?` jsonb operator for improved readability. +*/ + BEGIN + RETURN data ? key; + END; +$$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION __msar.get_schema_name(sch_id oid) RETURNS TEXT AS $$/* Return the name for a given schema, quoted as appropriate. @@ -126,6 +145,20 @@ END; $$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT; +CREATE OR REPLACE FUNCTION +__msar.get_fully_qualified_object_name(sch_name text, obj_name text) RETURNS text AS $$/* +Return the fully-qualified name for a given database object (e.g., table). + +Args: + sch_name: The schema of the object, quoted. + obj_name: The name of the object, unqualified and quoted. +*/ +BEGIN + RETURN format('%s.%s', sch_name, obj_name); +END; +$$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT; + + CREATE OR REPLACE FUNCTION msar.get_fully_qualified_object_name(sch_name text, obj_name text) RETURNS text AS $$/* Return the fully-qualified, properly quoted, name for a given database object (e.g., table). @@ -135,7 +168,7 @@ Args: obj_name: The name of the object, unqualified and unquoted. */ BEGIN - RETURN format('%s.%s', quote_ident(sch_name), quote_ident(obj_name)); + RETURN __msar.get_fully_qualified_object_name(quote_ident(sch_name), quote_ident(obj_name)); END; $$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT; @@ -449,6 +482,45 @@ SELECT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname=schema_name); $$ LANGUAGE SQL RETURNS NULL ON NULL INPUT; +---------------------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------- +-- ROLE MANIPULATION FUNCTIONS +-- +-- Functions in this section should always involve creating, granting, or revoking privileges or +-- roles +---------------------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------------- + + +-- Create mathesar user ---------------------------------------------------------------------------- + + +CREATE OR REPLACE FUNCTION +msar.create_basic_mathesar_user(username text, password_ text) RETURNS TEXT AS $$/* +*/ +DECLARE + sch_name text; + mathesar_schemas text[] := ARRAY['mathesar_types', '__msar', 'msar']; +BEGIN + PERFORM __msar.exec_ddl('CREATE USER %I WITH PASSWORD %L', username, password_); + PERFORM __msar.exec_ddl( + 'GRANT CREATE, CONNECT, TEMP ON DATABASE %I TO %I', + current_database()::text, + username + ); + FOREACH sch_name IN ARRAY mathesar_schemas LOOP + BEGIN + PERFORM __msar.exec_ddl('GRANT USAGE ON SCHEMA %I TO %I', sch_name, username); + EXCEPTION + WHEN invalid_schema_name THEN + RAISE NOTICE 'Schema % does not exist', sch_name; + END; + END LOOP; + RETURN username; +END; +$$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT; + + ---------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- -- ALTER SCHEMA FUNCTIONS @@ -899,7 +971,8 @@ CREATE TYPE __msar.col_def AS ( type_ text, -- The type of the column to create, fully specced with arguments. not_null boolean, -- A boolean to describe whether the column is nullable or not. default_ text, -- Text SQL giving the default value for the column. - identity_ boolean -- A boolean giving whether the column is an identity pkey column. + identity_ boolean, -- A boolean giving whether the column is an identity pkey column. + description text -- A text that will become a comment for the column ); @@ -956,7 +1029,8 @@ SELECT array_agg( -- set the default value for the duplicate column if specified CASE WHEN copy_defaults THEN pg_get_expr(adbin, tab_id) END, -- We don't set a duplicate column as a primary key, since that would cause an error. - false + false, + msar.col_description(tab_id, pg_columns.attnum) )::__msar.col_def ) FROM pg_attribute AS pg_columns @@ -1266,7 +1340,8 @@ The col_defs should have the form: "options": (optional), }, "not_null": (optional; default false), - "default": (optional) + "default": (optional), + "description": (optional) }, { ... @@ -1301,7 +1376,9 @@ WITH attnum_cte AS ( format('%L', col_def_obj ->> 'default') END, -- We don't allow setting the primary key column manually - false + false, + -- Set the description for the column + quote_literal(col_def_obj ->> 'description') )::__msar.col_def AS col_defs FROM attnum_cte, jsonb_array_elements(col_defs) AS col_def_obj WHERE col_def_obj ->> 'name' IS NULL OR col_def_obj ->> 'name' <> 'id' @@ -1310,8 +1387,9 @@ SELECT array_cat( CASE WHEN create_id THEN -- The below tuple defines a default 'id' column for Mathesar. It has name id, type integer, - -- it's not null, and it uses the 'identity' functionality to generate default values. - ARRAY[('id', 'integer', true, null, true)]::__msar.col_def[] + -- it's not null, it uses the 'identity' functionality to generate default values, has + -- a default comment. + ARRAY[('id', 'integer', true, null, true, 'Mathesar default ID column')]::__msar.col_def[] END, array_agg(col_defs) ) @@ -1350,9 +1428,20 @@ Args: */ DECLARE col_create_defs __msar.col_def[]; + fq_table_name text := __msar.get_relation_name(tab_id); BEGIN col_create_defs := msar.process_col_def_jsonb(tab_id, col_defs, raw_default); - PERFORM __msar.add_columns(__msar.get_relation_name(tab_id), variadic col_create_defs); + PERFORM __msar.add_columns(fq_table_name, variadic col_create_defs); + + PERFORM + __msar.comment_on_column( + fq_table_name, + col_create_def.name_, + col_create_def.description + ) + FROM unnest(col_create_defs) AS col_create_def + WHERE col_create_def.description IS NOT NULL; + RETURN array_agg(attnum) FROM (SELECT * FROM pg_attribute WHERE attrelid=tab_id) L INNER JOIN unnest(col_create_defs) R @@ -2215,10 +2304,12 @@ query. */ DECLARE r RECORD; - col_alter_str text; + col_alter_str TEXT; + description_alter RECORD; BEGIN -- Get the string specifying all non-name-change alterations to perform. col_alter_str := msar.process_col_alter_jsonb(tab_id, col_alters); + -- Perform the non-name-change alterations IF col_alter_str IS NOT NULL THEN PERFORM __msar.exec_ddl( @@ -2227,6 +2318,22 @@ BEGIN msar.process_col_alter_jsonb(tab_id, col_alters) ); END IF; + + -- Here, we perform all description-changing alterations. + FOR description_alter IN + SELECT + (col_alter->>'attnum')::integer AS col_id, + col_alter->>'description' AS comment_ + FROM jsonb_array_elements(col_alters) AS col_alter + WHERE __msar.jsonb_key_exists(col_alter, 'description') + LOOP + PERFORM msar.comment_on_column( + tab_id := tab_id, + col_id := description_alter.col_id, + comment_ := description_alter.comment_ + ); + END LOOP; + -- Here, we perform all name-changing alterations. FOR r in SELECT attnum, name FROM jsonb_to_recordset(col_alters) AS x(attnum integer, name text) LOOP @@ -2237,6 +2344,101 @@ END; $$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT; +-- Comment on column ------------------------------------------------------------------------------- + + +CREATE OR REPLACE FUNCTION +__msar.comment_on_column( + tab_name text, + col_name text, + comment_ text +) RETURNS text AS $$/* +Change the description of a column, returning command executed. If comment_ is NULL, column's +comment is removed. + +Args: + tab_name: The name of the table containg the column whose comment we will change. + col_name: The name of the column whose comment we'll change + comment_: The new comment. Any quotes or special characters must be escaped. +*/ +DECLARE + comment_or_null text := COALESCE(comment_, 'NULL'); +BEGIN +RETURN __msar.exec_ddl( + 'COMMENT ON COLUMN %s.%s IS %s', + tab_name, + col_name, + comment_or_null +); +END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION +msar.comment_on_column( + sch_name text, + tab_name text, + col_name text, + comment_ text +) RETURNS text AS $$/* +Change the description of a column, returning command executed. + +Args: + sch_name: The schema of the table whose column's comment we will change. + tab_name: The name of the table whose column's comment we will change. + col_name: The name of the column whose comment we will change. + comment_: The new comment. +*/ +SELECT __msar.comment_on_column( + msar.get_fully_qualified_object_name(sch_name, tab_name), + quote_ident(col_name), + quote_literal(comment_) +); +$$ LANGUAGE SQL; + + +CREATE OR REPLACE FUNCTION +__msar.comment_on_column( + tab_id oid, + col_id integer, + comment_ text +) RETURNS text AS $$/* +Change the description of a column, returning command executed. + +Args: + tab_id: The OID of the table containg the column whose comment we will change. + col_id: The ATTNUM of the column whose comment we will change. + comment_: The new comment. +*/ +SELECT __msar.comment_on_column( + __msar.get_relation_name(tab_id), + msar.get_column_name(tab_id, col_id), + comment_ +); +$$ LANGUAGE SQL; + + +CREATE OR REPLACE FUNCTION +msar.comment_on_column( + tab_id oid, + col_id integer, + comment_ text +) RETURNS text AS $$/* +Change the description of a column, returning command executed. + +Args: + tab_id: The OID of the table containg the column whose comment we will change. + col_id: The ATTNUM of the column whose comment we will change. + comment_: The new comment. +*/ +SELECT __msar.comment_on_column( + tab_id, + col_id, + quote_literal(comment_) +); +$$ LANGUAGE SQL; + + ---------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- -- MATHESAR LINK FUNCTIONS diff --git a/db/sql/__init__.py b/db/sql/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/db/sql/test_0_msar.sql b/db/sql/test_0_msar.sql index eacf740dbb..e21f4712f6 100644 --- a/db/sql/test_0_msar.sql +++ b/db/sql/test_0_msar.sql @@ -169,8 +169,8 @@ BEGIN RETURN NEXT is( msar.process_col_def_jsonb(0, '[{}, {}]'::jsonb, false), ARRAY[ - ('"Column 1"', 'text', null, null, false), - ('"Column 2"', 'text', null, null, false) + ('"Column 1"', 'text', null, null, false, null), + ('"Column 2"', 'text', null, null, false, null) ]::__msar.col_def[], 'Empty columns should result in defaults' ); @@ -182,12 +182,19 @@ BEGIN RETURN NEXT is( msar.process_col_def_jsonb(0, '[{}, {}]'::jsonb, false, true), ARRAY[ - ('id', 'integer', true, null, true), - ('"Column 1"', 'text', null, null, false), - ('"Column 2"', 'text', null, null, false) + ('id', 'integer', true, null, true, 'Mathesar default ID column'), + ('"Column 1"', 'text', null, null, false, null), + ('"Column 2"', 'text', null, null, false, null) ]::__msar.col_def[], 'Column definition processing add "id" column' ); + RETURN NEXT is( + msar.process_col_def_jsonb(0, '[{"description": "Some comment"}]'::jsonb, false), + ARRAY[ + ('"Column 1"', 'text', null, null, false, '''Some comment''') + ]::__msar.col_def[], + 'Comments should be sanitized' + ); END; $f$ LANGUAGE plpgsql; @@ -234,6 +241,25 @@ END; $f$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION test_add_columns_comment() RETURNS SETOF TEXT AS $f$ +DECLARE + col_name text := 'tcol'; + description text := 'Some; comment with a semicolon'; + tab_id integer := 'add_col_testable'::regclass::oid; + col_id integer; + col_create_arr jsonb; +BEGIN + col_create_arr := format('[{"name": "%s", "description": "%s"}]', col_name, description); + PERFORM msar.add_columns(tab_id, col_create_arr); + col_id := msar.get_attnum(tab_id, col_name); + RETURN NEXT is( + msar.col_description(tab_id, col_id), + description + ); +END; +$f$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION test_add_columns_multi_default_name() RETURNS SETOF TEXT AS $f$ /* This tests the default settings. When not given, the defautl column should be nullable and have no @@ -303,7 +329,6 @@ BEGIN END; $f$ LANGUAGE plpgsql; - CREATE OR REPLACE FUNCTION test_add_columns_interval_precision() RETURNS SETOF TEXT AS $f$ DECLARE col_create_arr jsonb := '[{"type": {"name": "interval", "options": {"precision": 6}}}]'; @@ -314,7 +339,7 @@ END; $f$ LANGUAGE plpgsql; --- Commented out to deal with upstream testing library bug. +-- upstream pgTAP bug: https://github.com/theory/pgtap/issues/315 -- CREATE OR REPLACE FUNCTION test_add_columns_interval_fields() RETURNS SETOF TEXT AS $f$ -- DECLARE -- col_create_arr jsonb := '[{"type": {"name": "interval", "options": {"fields": "year"}}}]'; @@ -393,15 +418,18 @@ BEGIN '42704', 'type "taxt" does not exist' ); - RETURN NEXT throws_ok( - format( - 'SELECT msar.add_columns(tab_id => %s, col_defs => ''%s'');', - 'add_col_testable'::regclass::oid, - '[{"type": {"name": "numeric", "options": {"scale": 23, "precision": 3}}}]'::jsonb - ), - '22023', - 'NUMERIC scale 23 must be between 0 and precision 3' - ); + RETURN NEXT CASE WHEN pg_version_num() < 150000 + THEN throws_ok( + format( + 'SELECT msar.add_columns(tab_id => %s, col_defs => ''%s'');', + 'add_col_testable'::regclass::oid, + '[{"type": {"name": "numeric", "options": {"scale": 23, "precision": 3}}}]'::jsonb + ), + '22023', + 'NUMERIC scale 23 must be between 0 and precision 3' + ) + ELSE skip('Numeric scale can be negative or greater than precision as of v15') + END; END; $f$ LANGUAGE plpgsql; @@ -528,6 +556,7 @@ END; $f$ LANGUAGE plpgsql; +-- upstream pgTAP bug: https://github.com/theory/pgtap/issues/315 -- CREATE OR REPLACE FUNCTION test_copy_column_interval_notation() RETURNS SETOF TEXT AS $f$ -- BEGIN -- PERFORM msar.copy_column( @@ -1580,7 +1609,8 @@ DECLARE "attnum": 2, "name": "nullab numeric", "not_null": false, - "type": {"name": "numeric", "options": {"precision": 8, "scale": 4}} + "type": {"name": "numeric", "options": {"precision": 8, "scale": 4}}, + "description": "This is; a comment with a semicolon!" }, {"attnum": 3, "name": "newcol2"}, {"attnum": 4, "delete": true}, @@ -1600,10 +1630,72 @@ BEGIN RETURN NEXT col_type_is('col_alters', 'col_opts', 'numeric(5,3)'); RETURN NEXT col_not_null('col_alters', 'col_opts'); RETURN NEXT col_not_null('col_alters', 'timecol'); + RETURN NEXT is(msar.col_description('col_alters'::regclass::oid, 2), 'This is; a comment with a semicolon!'); + RETURN NEXT is(msar.col_description('col_alters'::regclass::oid, 3), NULL); END; $f$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION test_comment_on_column() RETURNS SETOF TEXT AS $$ +DECLARE + change1 jsonb := $j$[ + { + "attnum": 2, + "description": "change1col2description" + }, + { + "attnum": 3, + "name": "change1col3name" + } + ]$j$; + change2 jsonb := $j$[ + { + "attnum": 2, + "description": "change2col2description" + }, + { + "attnum": 3, + "description": "change2col3description" + } + ]$j$; + -- Below change should not affect the description. + change3 jsonb := $j$[ + { + "attnum": 2, + "name": "change3col2name" + }, + { + "attnum": 3, + "name": "change3col3name" + } + ]$j$; + change4 jsonb := $j$[ + { + "attnum": 2, + "name": "change4col2name", + "description": null + }, + { + "attnum": 3, + "name": "change4col3name" + } + ]$j$; +BEGIN + RETURN NEXT is(msar.col_description('col_alters'::regclass::oid, 2), NULL); + PERFORM msar.alter_columns('col_alters'::regclass::oid, change1); + RETURN NEXT is(msar.col_description('col_alters'::regclass::oid, 2), 'change1col2description'); + PERFORM msar.alter_columns('col_alters'::regclass::oid, change2); + RETURN NEXT is(msar.col_description('col_alters'::regclass::oid, 2), 'change2col2description'); + PERFORM msar.alter_columns('col_alters'::regclass::oid, change3); + RETURN NEXT is(msar.col_description('col_alters'::regclass::oid, 2), 'change2col2description'); + RETURN NEXT is(msar.col_description('col_alters'::regclass::oid, 3), 'change2col3description'); + PERFORM msar.alter_columns('col_alters'::regclass::oid, change4); + RETURN NEXT is(msar.col_description('col_alters'::regclass::oid, 2), NULL); + RETURN NEXT is(msar.col_description('col_alters'::regclass::oid, 3), 'change2col3description'); +END; +$$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION setup_roster() RETURNS SETOF TEXT AS $$ BEGIN CREATE TABLE "Roster" ( @@ -1965,3 +2057,25 @@ BEGIN RETURN NEXT is(msar.is_default_possibly_dynamic(tab_id, 6), true); END; $$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION test_create_basic_mathesar_user() RETURNS SETOF TEXT AS $$ +BEGIN + PERFORM msar.create_basic_mathesar_user('testuser', 'mypass1234'); + RETURN NEXT database_privs_are ( + 'mathesar_testing', 'testuser', ARRAY['CREATE', 'CONNECT', 'TEMPORARY'] + ); + RETURN NEXT schema_privs_are ('msar', 'testuser', ARRAY['USAGE']); + RETURN NEXT schema_privs_are ('__msar', 'testuser', ARRAY['USAGE']); + PERFORM msar.create_basic_mathesar_user( + 'Ro"\bert''); DROP SCHEMA public;', 'my''pass1234"; DROP SCHEMA public;' + ); + RETURN NEXT has_schema('public'); + RETURN NEXT has_user('Ro"\bert''); DROP SCHEMA public;'); + RETURN NEXT database_privs_are ( + 'mathesar_testing', 'Ro"\bert''); DROP SCHEMA public;', ARRAY['CREATE', 'CONNECT', 'TEMPORARY'] + ); + RETURN NEXT schema_privs_are ('msar', 'Ro"\bert''); DROP SCHEMA public;', ARRAY['USAGE']); + RETURN NEXT schema_privs_are ('__msar', 'Ro"\bert''); DROP SCHEMA public;', ARRAY['USAGE']); +END; +$$ LANGUAGE plpgsql; diff --git a/db/types/base.py b/db/types/base.py index be2aab3a90..2024aaeeb6 100644 --- a/db/types/base.py +++ b/db/types/base.py @@ -8,8 +8,6 @@ class DatabaseType(OrderByIds): - value: str # noqa: NT001 - @property def id(self): """ diff --git a/db/types/install.py b/db/types/install.py index d830e08f77..2b081f5aa2 100644 --- a/db/types/install.py +++ b/db/types/install.py @@ -1,8 +1,8 @@ from db.types.custom import email, money, multicurrency, uri, json_array, json_object from db.types.base import SCHEMA from db.schemas.operations.create import create_schema -from db.schemas.operations.drop import drop_schema from db.types.operations.cast import install_all_casts +import psycopg def create_type_schema(engine): @@ -22,8 +22,6 @@ def install_mathesar_on_database(engine): def uninstall_mathesar_from_database(engine): - _cascade_type_schema(engine) - - -def _cascade_type_schema(engine): - drop_schema(SCHEMA, engine, cascade=True) + conn_str = str(engine.url) + with psycopg.connect(conn_str) as conn: + conn.execute(f"DROP SCHEMA IF EXISTS __msar, msar, {SCHEMA} CASCADE") diff --git a/db/utils.py b/db/utils.py index cdd34ec048..943b16fe8e 100644 --- a/db/utils.py +++ b/db/utils.py @@ -38,7 +38,7 @@ class OrderByIds: A mixin for ordering based on ids; useful at least for type enums in testing. """ - id: str # noqa: NT001 + id = None def __ge__(self, other): if self._ordering_supported(other): diff --git a/demo/install/datasets.py b/demo/install/datasets.py index df46d543eb..af8bcf84e2 100644 --- a/demo/install/datasets.py +++ b/demo/install/datasets.py @@ -1,15 +1,14 @@ """This module contains functions to load datasets for the demo.""" -import bz2 import logging import pickle from sqlalchemy import text from demo.install.arxiv_skeleton import setup_and_register_schema_for_receiving_arxiv_data +from demo.install.library_dataset import load_library_dataset +from demo.install.movies_dataset import load_movies_dataset from demo.management.commands.load_arxiv_data import update_arxiv_schema from demo.install.base import ( - LIBRARY_MANAGEMENT, LIBRARY_ONE, LIBRARY_TWO, - MOVIE_COLLECTION, MOVIES_SQL_BZ2, MATHESAR_CON, DEVCON_DATASET, ARXIV, ARXIV_PAPERS_PICKLE, ) @@ -17,42 +16,12 @@ def load_datasets(engine): """Load some SQL files with demo data to DB targeted by `engine`.""" - _load_library_dataset(engine) - _load_movies_dataset(engine) + load_library_dataset(engine) + load_movies_dataset(engine) _load_devcon_dataset(engine) _load_arxiv_data_skeleton(engine) -def _load_library_dataset(engine): - """ - Load the library dataset into a "Library Management" schema. - - Uses given engine to define database to load into. - Destructive, and will knock out any previous "Library Management" - schema in the given database. - """ - drop_schema_query = text(f"""DROP SCHEMA IF EXISTS "{LIBRARY_MANAGEMENT}" CASCADE;""") - create_schema_query = text(f"""CREATE SCHEMA "{LIBRARY_MANAGEMENT}";""") - set_search_path = text(f"""SET search_path="{LIBRARY_MANAGEMENT}";""") - with engine.begin() as conn, open(LIBRARY_ONE) as f1, open(LIBRARY_TWO) as f2: - conn.execute(drop_schema_query) - conn.execute(create_schema_query) - conn.execute(set_search_path) - conn.execute(text(f1.read())) - conn.execute(text(f2.read())) - - -def _load_movies_dataset(engine): - drop_schema_query = text(f"""DROP SCHEMA IF EXISTS "{MOVIE_COLLECTION}" CASCADE;""") - create_schema_query = text(f"""CREATE SCHEMA "{MOVIE_COLLECTION}";""") - set_search_path = text(f"""SET search_path="{MOVIE_COLLECTION}";""") - with engine.begin() as conn, bz2.open(MOVIES_SQL_BZ2, 'rt') as f: - conn.execute(drop_schema_query) - conn.execute(create_schema_query) - conn.execute(set_search_path) - conn.execute(text(f.read())) - - def _load_devcon_dataset(engine): drop_schema_query = text(f"""DROP SCHEMA IF EXISTS "{MATHESAR_CON}" CASCADE;""") create_schema_query = text(f"""CREATE SCHEMA "{MATHESAR_CON}";""") diff --git a/demo/install/library_dataset.py b/demo/install/library_dataset.py new file mode 100644 index 0000000000..14c1f39b32 --- /dev/null +++ b/demo/install/library_dataset.py @@ -0,0 +1,29 @@ +"""This module contains functions to load the Library Management dataset.""" + +from sqlalchemy import text +from demo.install.base import LIBRARY_MANAGEMENT, LIBRARY_ONE, LIBRARY_TWO + + +def load_library_dataset(engine, safe_mode=False): + """ + Load the library dataset into a "Library Management" schema. + + Args: + engine: an SQLAlchemy engine defining the connection to load data into. + safe_mode: When True, we will throw an error if the "Library Management" + schema already exists instead of dropping it. + + Uses given engine to define database to load into. + Destructive, and will knock out any previous "Library Management" + schema in the given database, unless safe_mode=True. + """ + drop_schema_query = text(f"""DROP SCHEMA IF EXISTS "{LIBRARY_MANAGEMENT}" CASCADE;""") + create_schema_query = text(f"""CREATE SCHEMA "{LIBRARY_MANAGEMENT}";""") + set_search_path = text(f"""SET search_path="{LIBRARY_MANAGEMENT}";""") + with engine.begin() as conn, open(LIBRARY_ONE) as f1, open(LIBRARY_TWO) as f2: + if safe_mode is False: + conn.execute(drop_schema_query) + conn.execute(create_schema_query) + conn.execute(set_search_path) + conn.execute(text(f1.read())) + conn.execute(text(f2.read())) diff --git a/demo/install/movies_dataset.py b/demo/install/movies_dataset.py new file mode 100644 index 0000000000..d5cb2a7e7e --- /dev/null +++ b/demo/install/movies_dataset.py @@ -0,0 +1,26 @@ +"""This module contains functions to load the Movie Collection dataset.""" +import bz2 + +from sqlalchemy import text + +from demo.install.base import MOVIE_COLLECTION, MOVIES_SQL_BZ2 + + +def load_movies_dataset(engine, safe_mode=False): + """ + Load the movie demo data set. + + Args: + engine: an SQLAlchemy engine defining the connection to load data into. + safe_mode: When True, we will throw an error if the "Movie Collection" + schema already exists instead of dropping it. + """ + drop_schema_query = text(f"""DROP SCHEMA IF EXISTS "{MOVIE_COLLECTION}" CASCADE;""") + create_schema_query = text(f"""CREATE SCHEMA "{MOVIE_COLLECTION}";""") + set_search_path = text(f"""SET search_path="{MOVIE_COLLECTION}";""") + with engine.begin() as conn, bz2.open(MOVIES_SQL_BZ2, 'rt') as f: + if safe_mode is False: + conn.execute(drop_schema_query) + conn.execute(create_schema_query) + conn.execute(set_search_path) + conn.execute(text(f.read())) diff --git a/demo/management/commands/load_arxiv_data.py b/demo/management/commands/load_arxiv_data.py index f8d3d19d28..7b0728c6e6 100644 --- a/demo/management/commands/load_arxiv_data.py +++ b/demo/management/commands/load_arxiv_data.py @@ -8,6 +8,7 @@ from demo.install.arxiv_skeleton import get_arxiv_db_and_schema_log_path from mathesar.database.base import create_mathesar_engine +from mathesar.models.base import Database class Command(BaseCommand): @@ -28,7 +29,8 @@ def update_our_arxiv_dbs(): logging.error(e, exc_info=True) return for db_name, schema_name in db_schema_pairs: - engine = create_mathesar_engine(db_name) + db = Database.current_objects.get(name=db_name) + engine = create_mathesar_engine(db) update_arxiv_schema(engine, schema_name, papers) engine.dispose() diff --git a/demo/management/commands/setup_demo_template_db.py b/demo/management/commands/setup_demo_template_db.py index 956bf13c65..78bb043644 100644 --- a/demo/management/commands/setup_demo_template_db.py +++ b/demo/management/commands/setup_demo_template_db.py @@ -1,11 +1,12 @@ from sqlalchemy import text - +from sqlalchemy.exc import OperationalError from django.conf import settings from django.core.management import BaseCommand from db.install import install_mathesar from demo.install.datasets import load_datasets from mathesar.database.base import create_mathesar_engine +from mathesar.models.base import Database class Command(BaseCommand): @@ -19,19 +20,34 @@ def _setup_demo_template_db(): print("Initializing demo template database...") template_db_name = settings.MATHESAR_DEMO_TEMPLATE - root_engine = create_mathesar_engine(settings.DATABASES["default"]["NAME"]) + django_model = Database.current_objects.get(name=settings.DATABASES["default"]["NAME"]) + root_engine = create_mathesar_engine(django_model) with root_engine.connect() as conn: conn.execution_options(isolation_level="AUTOCOMMIT") conn.execute(text(f"DROP DATABASE IF EXISTS {template_db_name} WITH (FORCE)")) root_engine.dispose() - install_mathesar( - database_name=template_db_name, - username=settings.DATABASES["default"]["USER"], - password=settings.DATABASES["default"]["PASSWORD"], - hostname=settings.DATABASES["default"]["HOST"], - port=settings.DATABASES["default"]["PORT"], - skip_confirm=True + db_model, _ = Database.current_objects.get_or_create( + name=template_db_name, + defaults={ + 'db_name': template_db_name, + 'username': django_model.username, + 'password': django_model.password, + 'host': django_model.host, + 'port': django_model.port + } ) - user_engine = create_mathesar_engine(template_db_name) + try: + install_mathesar( + database_name=template_db_name, + hostname=db_model.host, + username=db_model.username, + password=db_model.password, + port=db_model.port, + skip_confirm=True + ) + except OperationalError as e: + db_model.delete() + raise e + user_engine = create_mathesar_engine(db_model) load_datasets(user_engine) user_engine.dispose() diff --git a/demo/middleware.py b/demo/middleware.py index 818303bded..6dcd459dc6 100644 --- a/demo/middleware.py +++ b/demo/middleware.py @@ -24,20 +24,29 @@ def __init__(self, get_response): def __call__(self, request): sessionid = request.COOKIES.get('sessionid', None) db_name = get_name(str(sessionid)) - database, created = Database.current_objects.get_or_create(name=db_name) + database, created = Database.current_objects.get_or_create( + name=db_name, + defaults={ + 'db_name': db_name, + 'username': settings.DATABASES['default']['USER'], + 'password': settings.DATABASES['default']['PASSWORD'], + 'host': settings.DATABASES['default']['HOST'], + 'port': settings.DATABASES['default']['PORT'] + } + ) if created: create_demo_database( db_name, - settings.DATABASES["default"]["USER"], - settings.DATABASES["default"]["PASSWORD"], - settings.DATABASES["default"]["HOST"], - settings.DATABASES["default"]["NAME"], - settings.DATABASES["default"]["PORT"], + database.username, + database.password, + database.host, + settings.DATABASES['default']['NAME'], + database.port, settings.MATHESAR_DEMO_TEMPLATE ) append_db_and_arxiv_schema_to_log(db_name, ARXIV) reset_reflection(db_name=db_name) - engine = create_mathesar_engine(db_name) + engine = create_mathesar_engine(database) customize_settings(engine) load_custom_explorations(engine) engine.dispose() diff --git a/dev-run.sh b/dev-run.sh index 873c9797d9..c132ddf96e 100755 --- a/dev-run.sh +++ b/dev-run.sh @@ -1,6 +1,5 @@ #!/usr/bin/env bash - # For deployments, the DockerFile is configured to build the # client. Hence, instead of using this script, the web server # can be directly started. @@ -11,6 +10,6 @@ cd mathesar_ui npm run dev & cd .. -python install.py -s +python -m mathesar.install --skip-confirm python manage.py createsuperuser --no-input --username admin --email admin@example.com python manage.py runserver 0.0.0.0:8000 && fg diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e71a1a3dfb..248a0bff4b 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -10,7 +10,10 @@ services: build: context: . dockerfile: Dockerfile.devdb + args: + PG_VERSION: ${PG_VERSION-13} environment: + - PG_VERSION=${PG_VERSION-13} - POSTGRES_DB=mathesar_django - POSTGRES_USER=mathesar - POSTGRES_PASSWORD=mathesar @@ -19,25 +22,12 @@ services: volumes: - dev_postgres_data:/var/lib/postgresql/data - ./db/sql:/sql/ - test-service: - extends: - file: docker-compose.yml - service: service - environment: - - DJANGO_DATABASE_URL=postgres://mathesar:mathesar@mathesar_dev_db:5432/mathesar_django - - MATHESAR_DATABASES=(mathesar_tables|postgresql://mathesar:mathesar@mathesar_dev_db:5432/mathesar) - container_name: mathesar_service_test - image: mathesar/mathesar-test:latest - build: - context: . - dockerfile: Dockerfile - args: - PYTHON_REQUIREMENTS: requirements-dev.txt - depends_on: - - dev-db - # On testing, the HTTP port is exposed to other containers, and the host. - ports: - - "8000:8000" + healthcheck: + test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB-mathesar_django} -U $${POSTGRES_USER-mathesar}"] + interval: 5s + timeout: 1s + retries: 30 + start_period: 5s # A Django development webserver + Svelte development server used when developing Mathesar. # The code changes are hot reloaded and debug flags are enabled to aid developers working on Mathesar. # It is not recommended to use this service in production environment. @@ -49,28 +39,50 @@ services: dockerfile: Dockerfile args: PYTHON_REQUIREMENTS: requirements-dev.txt - extends: - file: docker-compose.yml - service: service environment: - MODE=${MODE-DEVELOPMENT} - DEBUG=${DEBUG-True} - - DJANGO_ALLOW_ASYNC_UNSAFE=true - - DJANGO_SUPERUSER_PASSWORD=password - DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE-config.settings.development} - - DJANGO_DATABASE_URL=postgres://mathesar:mathesar@mathesar_dev_db:5432/mathesar_django + - ALLOWED_HOSTS=${ALLOWED_HOSTS-*} + - SECRET_KEY=${SECRET_KEY} - MATHESAR_DATABASES=(mathesar_tables|postgresql://mathesar:mathesar@mathesar_dev_db:5432/mathesar) - entrypoint: dockerize -wait tcp://mathesar_dev_db:5432 -timeout 30s ./dev-run.sh + - DJANGO_SUPERUSER_PASSWORD=password + - POSTGRES_DB=mathesar_django + - POSTGRES_USER=mathesar + - POSTGRES_PASSWORD=mathesar + - POSTGRES_HOST=mathesar_dev_db + - POSTGRES_PORT=5432 + entrypoint: ./dev-run.sh volumes: - .:/code/ - ui_node_modules:/code/mathesar_ui/node_modules/ depends_on: - - dev-db + dev-db: + condition: service_healthy # On dev, following ports are exposed to other containers, and the host. ports: - "8000:8000" - "3000:3000" - "6006:6006" + test-service: + container_name: mathesar_service_test + image: mathesar/mathesar-test:latest + environment: + - MATHESAR_DATABASES=(mathesar_tables|postgresql://mathesar:mathesar@mathesar_dev_db:5432/mathesar) + - POSTGRES_DB=mathesar_django + - POSTGRES_USER=mathesar + - POSTGRES_PASSWORD=mathesar + - POSTGRES_HOST=mathesar_dev_db + - POSTGRES_PORT=5432 + build: + context: . + dockerfile: Dockerfile + args: + PYTHON_REQUIREMENTS: requirements-dev.txt + depends_on: + - dev-db + ports: + - "8000:8000" volumes: ui_node_modules: dev_postgres_data: diff --git a/docker-compose.yml b/docker-compose.yml index 95dac1f7c6..f191da922d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,100 +1,239 @@ version: "3.9" -# Please refer the documentation located at https://docs.mathesar.org/installation/docker-compose/ for the setting up Mathesar using this docker-compose.yml. + +# This file defines a viable production setup for Mathesar. +# +# It can be used in production directly, or used as an example to help define +# your own infrastructure. +# +#------------------------------------------------------------------------------- +# PREREQUISITES +# +# Please double-check that your docker setup meets the following criteria: +# +# OS: Linux, Mac, Windows(WSL). +# Docker v23+ $ docker version +# Docker Compose v2.10+ $ docker compose version +# +#------------------------------------------------------------------------------- +# HOW TO USE THIS FILE +# +# First, make sure you meet the prerequisites, add a secret key below, and then +# run: +# +# $ docker compose -f docker-compose.yml up +# +# Note: You may need to run Docker commands using sudo, depending on your setup. +# Running Docker in rootless mode isn't currently supported. +# +#------------------------------------------------------------------------------- +# CONFIG +# +# Customize your Mathesar installation with the following variables. +# See https://docs.mathesar.org/configuration/env-variables/ for more info. +# +x-config: &config + # (REQUIRED) Replace '?' with '-' followed by a 50 character random string. + # You can generate one at https://djecrety.ir/ or by running: + # echo $(cat /dev/urandom | LC_CTYPE=C tr -dc 'a-zA-Z0-9' | head -c 50) + SECRET_KEY: ${SECRET_KEY:?} + + # (Optional) Replace 'http://localhost' with custom domain(s) e.g. + # 'yourdomain.com, 127.0.0.1' to manage the host(s) at which you want to + # access Mathesar over http or https + DOMAIN_NAME: ${DOMAIN_NAME:-http://localhost} + + # Edit the POSTGRES_* variables if you are not using the db service provided + # below, or if you want to use a custom database user. + + # (Optional) Replace 'mathesar_django' with any custom name for the internal + # database managed by mathesar web-service + POSTGRES_DB: ${POSTGRES_DB:-mathesar_django} + + # (Optional) Replace 'mathesar' with any custom username for the + # aforementioned database + POSTGRES_USER: ${POSTGRES_USER:-mathesar} + + # (Optional) Replace 'mathesar' with any custom password for the + # aforementioned database + + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mathesar} + # (Optional) Replace 'mathesar_db' with the name of the host running postgres + POSTGRES_HOST: ${POSTGRES_HOST:-mathesar_db} + + # (Optional) Replace '5432' with the port on which postgres is running + POSTGRES_PORT: ${POSTGRES_PORT:-5432} + +#------------------------------------------------------------------------------- +# ADDITIONAL INFO ABOUT CONFIG VARIABLES +# +# SECRET_KEY: +# Default: N/A +# Info: A unique secret key required to be set by the user for Django's +# security protection features. It should be 50 random characters. You +# can read more about it here: +# https://docs.djangoproject.com/en/4.2/ref/settings/#secret-key +# Example: a_very_insecure_secret_key1*zobb123)k(_d1%wubkv6# +# +# DOMAIN_NAME: +# Default: http://localhost +# Info: Specifies the domains that can access Mathesar over http(port 80) +# or https(port 443), also automatically creating SSL certificates +# for the same. If you want to host an instance of Mathesar over the +# internet or over your local network, add those domains here. +# Example: yourdomain.com, *.subdomain.com, 127.0.0.1 +# +# POSTGRES_DB: +# Default: mathesar_django +# Info: Specifies a name for the database that will be created and used by +# Mathesar for managing internal data. +# Example: zeus +# +# POSTGRES_USER: +# Default: mathesar +# Info: Specifies creation of a user with superuser privileges +# and a database with the same name. +# Example: athena +# +# POSTGRES_PASSWORD: +# Default: mathesar +# Info: Specifies the superuser password that is required to be set for the +# PostgreSQL docker image. +# Example: apollo +# +# POSTGRES_HOST: +# Default: mathesar_db (name of the db service provided below) +# Info: Specifies the host name on which portgres listen for connections +# from client applications. +# Example: kratos +# +# POSTGRES_PORT: +# Default: 5432 +# Info: Specifies the port on which portgres listen for connections from +# client applications. +# Example: 5555 +# +#------------------------------------------------------------------------------- +# INFO ABOUT VOLUMES +# +# Volumes are used by Mathesar to persist essential data. +# +# Running this compose file will automatically create a subdirectory named +# "msar" with the following file structure: +# +# msar +# ├── caddy/ (stores certificates, keys, and other information for Caddy) +# ├── media/ (stores user uploaded datafiles(.csv/.tsv) to Mathesar) +# ├── pgdata/ (stores PostgreSQL data) +# └── static/ (stores static files for Mathesar) +# +#------------------------------------------------------------------------------- +# MATHESAR SERVICES +# +# The next section defines various containers in a workable production setup. +# services: - db: - image: postgres:13 - container_name: mathesar_db - environment: - # These environment variables are used to create a database and superuser when the `db` service starts. - # Refer to https://hub.docker.com/_/postgres for more information on these variables. - - POSTGRES_DB=${POSTGRES_DB-mathesar_django} - - POSTGRES_USER=${POSTGRES_USER-mathesar} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD-mathesar} - expose: - - "5432" - volumes: - - postgresql_data:/var/lib/postgresql/data - healthcheck: - test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB-mathesar_django} -U $${POSTGRES_USER-mathesar}"] - interval: 5s - timeout: 1s - retries: 30 - start_period: 5s - # A caddy reverse proxy sitting in-front of all the services. - # It is responsible for routing traffic to the respective services - # It is also responsible for serving static files, automatically providing SSL certificate - # and preventing certain DDOS attacks - caddy-reverse-proxy: - image: mathesar/mathesar-caddy:latest - environment: - - DOMAIN_NAME=${DOMAIN_NAME-http://localhost} - # caddy container is exposed to the other containers, and the host. - ports: - - "${HTTP_PORT-80}:80" - - "${HTTPS_PORT-443}:443" - volumes: - - media:/mathesar/media - - static:/mathesar/static - - caddy_data:/data - - caddy_config:/config - labels: - - "com.centurylinklabs.watchtower.enable=true" - # A gunicorn WSGI HTTP Server that - # runs the Mathesar App. - # It depends on the `db` service - # and will start the `db` service automatically before starting itself. + #----------------------------------------------------------------------------- + # Mathesar web service + # + # This service provides the main web server required to run Mathesar, using + # our official Docker image hosted on Docker Hub + # + # As configured, this service exposes port 8000 to other services but not the + # host system. This isolates it from being directly accessed by the host + # while allowing access via caddy. + # service: container_name: mathesar_service image: mathesar/mathesar-prod:latest - environment: - - MODE=${MODE-PRODUCTION} - - DEBUG=${DEBUG-False} - - DJANGO_ALLOW_ASYNC_UNSAFE=true - - DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE-config.settings.production} - - ALLOWED_HOSTS=${ALLOWED_HOSTS-*} - - SECRET_KEY=${SECRET_KEY} - - DJANGO_DATABASE_URL=${DJANGO_DATABASE_URL-postgres://mathesar:mathesar@mathesar_db:5432/mathesar_django} - - MATHESAR_DATABASES=${MATHESAR_DATABASES-(mathesar_tables|postgresql://mathesar:mathesar@mathesar_db:5432/mathesar)} - - DJANGO_SUPERUSER_PASSWORD=${DJANGO_SUPERUSER_PASSWORD} + environment: + # First we load the variables configured above. + <<: *config + + DJANGO_SETTINGS_MODULE: config.settings.production + + # We set ALLOWED_HOSTS to * (allow all hosts) by default here since we are + # relying on caddy to manage which domains could access the mathesar web + # service. If you do not want to use caddy add the domain(s) that you + # want to ALLOWED_HOSTS. Doing so will restrict traffic from all other + # domains. + ALLOWED_HOSTS: ${ALLOWED_HOSTS:-*} + + # WARNING: MATHESAR_DATABASES is deprecated, and will be removed in a future release. + MATHESAR_DATABASES: ${MATHESAR_DATABASES:-} entrypoint: ./run.sh volumes: - - static:/code/static - - media:/code/media + - ./msar/static:/code/static + - ./msar/media:/code/media depends_on: - # Comment the below field to disable starting the database service automatically db: condition: service_healthy - labels: - - "com.centurylinklabs.watchtower.enable=true" healthcheck: test: curl -f http://localhost:8000 interval: 10s timeout: 5s retries: 30 start_period: 5s - # On prod, the HTTP port is exposed to other containers, but not the host to prevent any unnecessary conflicts with external services. - # Do not make any changes to this port + # If using caddy, expose the internal port 8000 only to other containers and + # not the docker host. expose: - "8000" - # A webserver responsible for - # receiving upgrade requests and upgrading the Mathesar App docker image. - # It upgrades the docker image only when a http request is sent to it - # For more information refer https://containrrr.dev/watchtower/http-api-mode/ - watchtower: - image: containrrr/watchtower - volumes: - - /var/run/docker.sock:/var/run/docker.sock - command: --http-api-update --label-enable --debug - environment: - - WATCHTOWER_HTTP_API_TOKEN=mytoken - labels: - - "com.centurylinklabs.watchtower.enable=false" - # Watchtower HTTP API is exposed to other containers, but not the host. + # Uncomment the following if not using caddy + # ports: + # - ${HOST_PORT:-8000}:8000 + + #----------------------------------------------------------------------------- + # PostgreSQL Database + # + # This service provides a Postgres database instance for holding both internal + # Mathesar data, as well as user data if desired, using the official + # PostgreSQL image hosted on Docker Hub + # + # As configured, this service exposes Postgres' default port (5432) to other + # services, allowing the Mathesar web sevice to connect to it. + # + db: + image: postgres:13 + container_name: mathesar_db + # This service needs the config variables defined above. + environment: *config + # Expose the internal port 5432 only to other containers and not + # the underlying host. expose: - - "8080" -volumes: - postgresql_data: - media: - static: - caddy_data: - caddy_config: + - "5432" + volumes: + - ./msar/pgdata:/var/lib/postgresql/data + healthcheck: + test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] + interval: 5s + timeout: 1s + retries: 30 + start_period: 5s + + #----------------------------------------------------------------------------- + # Caddy + # + # This service provides a reverse proxy for the Mathesar web server, using our + # custom Caddy image hosted on Docker Hub. That image is customized to use a + # Caddyfile with an appropriate configuration for Mathesar. + # + # Specifically, this service routes the requests to backend and the web + # frontend of Mathesar while also serving essential staic files and user + # uploaded datafiles(.csv/.tsv). It also provides SSL certificates + # automatically for any custom domain(s) listed in DOMAIN_NAME that you might + # want to use to access Mathesar. + # + # This service maps the default port for http(80) and https(443) of the host + # system to that of docker's for allowing access to Mathesar over http or + # https. + # + caddy-reverse-proxy: + image: mathesar/mathesar-caddy:latest + # This service needs the config variables defined above. + environment: *config + ports: + - "80:80" + - "443:443" + volumes: + - ./msar/media:/code/media + - ./msar/static:/code/static + - ./msar/caddy:/data diff --git a/docs/docs/administration/uninstall.md b/docs/docs/administration/uninstall.md index 6c4098e6f6..ae63cd0521 100644 --- a/docs/docs/administration/uninstall.md +++ b/docs/docs/administration/uninstall.md @@ -2,7 +2,98 @@ The uninstall instructions vary depending on the [installation method](../index.md#installing-mathesar) you chose. Select your installation method below to proceed. -- [Uninstall a **guided** installation of Mathesar](../installation/guided-install/index.md#uninstall) -- [Uninstall a **Docker compose** installation of Mathesar](../installation/docker-compose/index.md#uninstall) -- [Uninstall a **Docker** installation of Mathesar](../installation/docker/index.md#uninstall) -- [Uninstall a **source-built** installation of Mathesar](../installation/build-from-source/index.md#uninstall) +## Uninstall a Docker installation of Mathesar + +!!!note + Depending on your Docker setup, you may need to run `docker` commands with `sudo`. + +1. Remove the Mathesar container. + + ``` + docker rm -v mathesar_service + ``` + +1. Remove the Mathesar Image + + ``` + docker rmi mathesar_service + ``` + +1. Remove volumes related to Mathesar + + ``` + docker volume rm static && + docker volume rm media + ``` + +{% include 'snippets/uninstall-schemas.md' %} + + +## Uninstall a Guided script or Docker compose installation of Mathesar + +1. Remove all Mathesar Docker images and containers. + + ``` + docker compose -f docker-compose.yml down --rmi all -v + ``` + +1. Remove configuration files. + + ``` + rm -rf xMATHESAR_INSTALLATION_DIRx # may need sudo, depending on location + ``` + +{% include 'snippets/uninstall-schemas.md' %} + +## Uninstall a source installation of Mathesar + +1. Stop Caddy service + + ``` + systemctl disable caddy.service && systemctl stop caddy.service + ``` + +1. Remove Caddy service file and Caddyfile (requires `sudo`) + + ``` + sudo rm /lib/systemd/system/caddy.service + sudo rm /etc/caddy/Caddyfile + ``` + +1. Stop Gunicorn + + ``` + systemctl disable gunicorn.service + systemctl stop gunicorn.service + ``` + +1. Remove Gunicorn service file + + ``` + sudo rm /lib/systemd/system/gunicorn.service + ``` + +1. Remove your Mathesar installation directory + + ``` + rm -r xMATHESAR_INSTALLATION_DIRx # May need sudo, depending on location + ``` + + !!! warning "Your installation directory might be customized" + It's possible that Mathesar could have been installed into a different directory than shown above. Use caution when deleting this directory. + +1. Remove Django database + + 1. Connect to the psql terminal. + + ``` + sudo -u postgres psql + ``` + + 2. Drop the Django database. + + ```postgresql + DROP DATABASE mathesar_django; + ``` + +{% include 'snippets/uninstall-schemas.md' %} diff --git a/docs/docs/administration/upgrade.md b/docs/docs/administration/upgrade.md deleted file mode 100644 index 40adf5154d..0000000000 --- a/docs/docs/administration/upgrade.md +++ /dev/null @@ -1,19 +0,0 @@ -# Upgrade Mathesar - -## Upgrade Mathesar via the web interface - -!!! note - In-app upgrades are only possible after installing Mathesar via our [**guided script**](../installation/guided-install/index.md) or our [**Docker compose**](../installation/docker-compose/index.md) instructions. - -1. Open the Settings menu at the top right of the screen, and click on **Administration**. -1. You should now see the "Software Update" page. -1. If a new version of Mathesar can be installed automatically, then you will see a "New Version Available" box containing an **Upgrade** button. Click the button to begin the upgrade, and follow the on-screen instructions after that. - -## Upgrade Mathesar via the command line - -The upgrade instructions vary depending on the [installation method](../index.md#installing-mathesar) you chose. Select your installation method below to proceed. - -- [Upgrade a **guided** installation of Mathesar](../installation/guided-install/index.md#upgrade) -- [Upgrade a **Docker compose** installation of Mathesar](../installation/docker-compose/index.md#upgrade) -- [Upgrade a **Docker** installation of Mathesar](../installation/docker/index.md#upgrade) -- [Upgrade a **source-built** installation of Mathesar](../installation/build-from-source/index.md#upgrade) diff --git a/docs/docs/administration/upgrade/0.1.4.md b/docs/docs/administration/upgrade/0.1.4.md new file mode 100644 index 0000000000..9fd47fd678 --- /dev/null +++ b/docs/docs/administration/upgrade/0.1.4.md @@ -0,0 +1,206 @@ +# Upgrade Mathesar to 0.1.4 + +The 0.1.4 release requires more upgrade steps than we hope to have for future releases! If you run into any trouble, we encourage you to [open an issue](https://github.com/mathesar-foundation/mathesar/issues/new/choose) or [contact us](https://mathesar.org/free-install.html) for help. + +## For installations using Docker Compose {:#docker-compose} + +If you followed our [Docker Compose installation instructions](../../installation/docker-compose/index.md), then use these steps to upgrade your installation to 0.1.4. + +!!! note + Depending on your setup, you may need to run some commands with `sudo`. + +1. Find needed parts + + 1. Find your `.env` and `docker-compose.yml` files. Run + + ``` + docker inspect mathesar_service + ``` + + and look for the value of the `"com.docker.compose.project.config_files"` key in the resulting JSON to find the path to the `docker-compose.yml` file. The `.env` file should be in the same directory. If you have `jq` installed, you can run + + ``` + docker inspect mathesar_service \ + | jq '.[0].Config.Labels."com.docker.compose.project.config_files"' + ``` + + and get the path directly. The `.env` file should be in the same directory. + + 1. Copy the path of the directory containing `docker-compose.yml` and `.env` into the box below. Do not include a trailing slash. + + + + Then press Enter to customize this guide with the configuration directory. + + 1. If you are using a Docker container for your PostgreSQL database, Run + + ``` + docker volume inspect mathesar_postgresql_data + ``` + + and look for the `"Mountpoint"` in the resulting JSON. + + 1. Copy the path of the directory into the box below. Do not include a trailing slash. + + + + Then press Enter to customize this guide with the PostgreSQL data directory. + +1. Stop Mathesar, remove old images + + ``` + docker compose -f xMATHESAR_INSTALLATION_DIRx/docker-compose.yml down --rmi all + ``` + +1. Set up new configuration + + !!! warning + `MATHESAR_DATABASES` has been deprecated as of v0.1.4 and will be removed entirely in future releases of Mathesar. If you end up deleting the variable from your `.env` file before starting up Mathesar after the upgrade, you can still add the connections manually through Mathesar's UI. + + 1. Back up the old configuration files: + + ``` + mv xMATHESAR_INSTALLATION_DIRx/docker-compose.yml xMATHESAR_INSTALLATION_DIRx/docker-compose.yml.backup + cp xMATHESAR_INSTALLATION_DIRx/.env xMATHESAR_INSTALLATION_DIRx/env.backup + ``` + + (We'll modify the old file, so we copy instead of moving it.) + + 1. Download the new docker compose file: + + ``` + curl -sfL -o xMATHESAR_INSTALLATION_DIRx/docker-compose.yml https://raw.githubusercontent.com/mathesar-foundation/mathesar/0.1.4/docker-compose.yml + ``` + + 1. Edit the `xMATHESAR_INSTALLATION_DIRx/.env` file to break the `DJANGO_DATABASE_URL` variable into its parts. + + This variable should have the form: + + ``` + DJANGO_DATABASE_URL=postgres://:@:/ + ``` + + You should edit the `.env` file to have the variables: + + ``` + POSTGRES_USER= + POSTGRES_PASSWORD= + POSTGRES_HOST= + POSTGRES_PORT= + POSTGRES_DB= + ``` + + If you don't want to set those environment variables (e.g., if they're otherwise used), you can instead edit the `docker-compose.yml` file directly to add those variables. + + 1. Double-check the rest of the configuration: + + - You should have your [`SECRET_KEY` variable](../../configuration/env-variables.md#secret_key) defined. + - If hosting on the internet, you should have a `DOMAIN_NAME` variable defined. + +1. Initialize new Mathesar installation + + ``` + docker compose -f xMATHESAR_INSTALLATION_DIRx/docker-compose.yml up -d + ``` + + This will pull new images, and start the Mathesar containers. Wait a few minutes, then run `docker ps` to verify that you have `mathesar_service`, `mathesar-caddy-reverse-proxy-1`, and `mathesar_db` running and that the service is healthy. The services should not be reporting errors. If you were _not_ using Docker volumes for your Mathesar PostgreSQL data, you're done, and you can login to Mathesar via your usual method. If you're not sure, try to login to Mathesar. If you're presented with a screen instructing you to create an Admin user, you likely need to proceed to the next step. + +1. Move your PostgreSQL directory + + 1. Bring down the services: + + ``` + docker compose -f xMATHESAR_INSTALLATION_DIRx/docker-compose.yml down + ``` + + 1. Remove scaffold database data, copy your old PostgreSQL volume to the new location: + + ``` + rm -r xMATHESAR_INSTALLATION_DIRx/msar/pgdata + cp -r xMATHESAR_PG_DIRx xMATHESAR_INSTALLATION_DIRx/msar/pgdata + ``` + + 1. Bring the services back up: + + ``` + docker compose -f xMATHESAR_INSTALLATION_DIRx/docker-compose.yml up -d + ``` + +1. If things look good, then you can try to login at the usual address using your normal username and password, and you should see your data. + +## For installations done via our guided script {:#guided} + +If you installed Mathesar with our (now deprecated) guided script, then you have a Docker Compose installation. See the [Docker Compose upgrade steps](#docker-compose). + + +## For installations done from scratch {:#scratch} + +If you installed Mathesar [from scratch](../../installation/build-from-source/index.md), then use these steps to upgrade your installation to 0.1.4. + +!!! warning + These steps have not yet been tested extensively. If you run into any trouble, we encourage you to [open an issue](https://github.com/mathesar-foundation/mathesar/issues/new/choose) or submit a PR proposing changes to [this file](https://github.com/mathesar-foundation/mathesar/blob/master/docs/docs/administration/upgrade/0.1.4.md). + +1. Go to your Mathesar installation directory. + + ``` + cd xMATHESAR_INSTALLATION_DIRx + ``` + + !!! note + Your installation directory may be different from above if you used a different directory when installing Mathesar. + +1. Pull the latest version from the repository + + ``` + git pull https://github.com/mathesar-foundation/mathesar.git + ``` + +1. Update Python dependencies + + ``` + pip install -r requirements.txt + ``` + +1. Next we will activate our virtual environment: + + ``` + source ./mathesar-venv/bin/activate + ``` + +1. Update your environment variables according to the [the new configuration specification](../../configuration/env-variables.md#db). In particular, you must put the connection info for the internal DB into new `POSTGRES_*` variables. The `DJANGO_DATABASE_URL` variable is no longer supported. + +1. Add the environment variables to the shell before running Django commands + + ``` + export $(sudo cat .env) + ``` + +1. Run the latest Django migrations + + ``` + python manage.py migrate + ``` + +1. Install the frontend dependencies + + ``` + npm ci --prefix mathesar_ui + ``` + +1. Build the Mathesar frontend app + + ``` + npm run --prefix mathesar_ui build --max_old_space_size=4096 + ``` + +1. Update Mathesar functions on the database: + + ``` + python mathesar/install.py --skip-confirm >> /tmp/install.py.log + ``` + +1. Restart the gunicorn server + + ``` + systemctl restart gunicorn + ``` diff --git a/docs/docs/administration/upgrade/older.md b/docs/docs/administration/upgrade/older.md new file mode 100644 index 0000000000..bb16281993 --- /dev/null +++ b/docs/docs/administration/upgrade/older.md @@ -0,0 +1,17 @@ +# Upgrading Mathesar to older versions + +## For installations using Docker Compose {:#docker-compose} + +If you have a Docker compose installation (including one from the guided script), run the command below: + +``` +docker compose -f /etc/mathesar/docker-compose.yml up \ + --force-recreate --build service +``` + +!!! note "Your installation directory may be different" + You may need to change `/etc/mathesar/` in the command above if you chose to install Mathesar to a different directory. + +## For installations done from scratch {:#scratch} + +If you installed from scratch, the upgrade instructions are the same as [for 0.1.4](../upgrade/0.1.4.md#scratch), but you do not need to change the environment variables. \ No newline at end of file diff --git a/docs/docs/configuration/connect-to-existing-db.md b/docs/docs/configuration/connect-to-existing-db.md index c0da25dfea..4bd27f7631 100644 --- a/docs/docs/configuration/connect-to-existing-db.md +++ b/docs/docs/configuration/connect-to-existing-db.md @@ -6,15 +6,13 @@ psql -c 'create database mathesar_django;' ``` -1. Configure the [`DJANGO_DATABASE_URL` environment variable](./env-variables.md#django_database_url) to point to the database you just created. - -1. (Optional) For Docker Compose related installations, you may [disable Mathesar's default database server](./customize-docker-compose.md#disable-db-service) if you like. +1. Configure the [Internal database environment variables](./env-variables.md#db) to point to the database you just created. ## Connect to a database server running on the host {: #localhost-db } !!! info "" - This content is related to Mathesar running in Docker related environments. This is applicable for the [Guided installation method](../installation/guided-install/index.md), [Docker Compose installation method](../installation/docker-compose/index.md), and [Docker installation method](../installation/docker/index.md). + This content is related to Mathesar running in Docker related environments. If you're running Mathesar in a Docker related environment, and your database server runs on the host machine, you will not be able to connect to it using `localhost:`, since `localhost` would refer to the Docker environment and not to the host. @@ -67,7 +65,7 @@ You can try using `host.docker.internal` instead of `localhost`. Below are detai === "Linux" ``` - sudo systemctl restart postgresql + systemctl restart postgresql ``` === "MacOS" ``` diff --git a/docs/docs/configuration/customize-docker-compose.md b/docs/docs/configuration/customize-docker-compose.md deleted file mode 100644 index 9e3b66c35d..0000000000 --- a/docs/docs/configuration/customize-docker-compose.md +++ /dev/null @@ -1,99 +0,0 @@ -# Customize Docker Compose related installations - -!!! info "" - This document is related to Mathesar running in Docker Compose related environments. This is applicable for the [Guided Installation method](../installation/guided-install/index.md), and [Docker Compose Installation method](../installation/docker-compose/index.md). - -### Default database server {: #default-db} - -The default `docker-compose.yml` includes a `db` service that automatically starts a Postgres database server container called `mathesar_db`. This service allows you to start using Mathesar immediately to store data in a Postgres database without administering a separate Postgres server outside Mathesar. - -The `db` service runs on the [internal docker compose port](https://docs.docker.com/compose/compose-file/compose-file-v3/#expose) `5432`. The internal port is not bound to the host to avoid conflicts with other services running on port `5432`. - -Additionally, it comes with a default database and a superuser. This database can come in handy for storing Mathesar's [metadata](./env-variables.md#django_database_url). The credentials for the Default database are: - -``` -DATABASE_NAME='mathesar_django' -USER='mathesar' -PASSWORD='mathesar' -``` - -you can [disable the default database server](#disable-db-service) if you plan on using an [existing database server](../configuration/connect-to-existing-db.md). - -### Disable the default database server {: #disable-db-service} - -The default `docker-compose.yml` automatically starts a [Postgres database server container](#default-db). You may disable it if you plan on using a different Database server. - -In the `docker-compose.yml` file, comment out the `db` services and the `depends_on` field of the `service`. - -```yaml hl_lines="2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 29 30 31" -services: - # db: - # image: postgres:13 - # container_name: mathesar_db - # environment: - # # These environment variables are used to create a database and superuser when the `db` service starts. - # # Refer to https://hub.docker.com/_/postgres for more information on these variables. - # - POSTGRES_DB=${POSTGRES_DB-mathesar_django} - # - POSTGRES_USER=${POSTGRES_USER-mathesar} - # - POSTGRES_PASSWORD=${POSTGRES_PASSWORD-mathesar} - # expose: - # - "5432" - # volumes: - # - postgresql_data:/var/lib/postgresql/data - # healthcheck: - # test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB-mathesar_django} -U $${POSTGRES_USER-mathesar}"] - # interval: 5s - # timeout: 1s - # retries: 30 - # start_period: 5s - - # ... - service: - # ... - volumes: - - static:/code/static - - media:/code/media - # Comment the below field to disable starting the database service automatically - # depends_on: - # db: - # condition: service_healthy -``` - -After this change, Mathesar will no longer start the `db` service automatically. - -### Run Mathesar on a non-standard HTTP port {: #non-standard-port} - -By default, Caddy serves the Mathesar web application on a port as determined by the protocol within your [`DOMAIN_NAME` environment variable](./env-variables.md#domain_name). - -- For `http` domain names it uses port `80`. -- For `https` domain names (as is the default, if not specified) it uses port `443` and redirects any traffic pointed at `http` to `https`. In this case, Caddy also creates an SSL certificate [automatically](https://caddyserver.com/docs/automatic-https#activation). - - !!! warning - If you don't have access to port `443`, avoid using `https` domain names on a non-standard port. Due to the following reasons: - - - Caddy won't be able to verify the SSL certificate when running on a non-standard port. - - Browsers automatically redirect traffic sent to the `http` domain to the standard `https` port (443), rather than to any non-standard `HTTPS_PORT` port that you may have configured. - -To use a non-standard port: - -1. Edit your `.env` file and set either the [`HTTP_PORT`](./env-variables.md#http_port) or the [`HTTPS_PORT`](./env-variables.md#https_port) environment variable (depending on the protocol you're using). - - !!! example - To serve Mathesar at `http://localhost:9000`, include the following in your `.env` file: - - ```bash - DOMAIN_NAME='http://localhost' - HTTP_PORT=9000 - ``` - -1. Restart the container - - === "Linux" - ``` - sudo docker compose -f docker-compose.yml up caddy-reverse-proxy -d - ``` - - === "MacOS" - ``` - docker compose -f docker-compose.yml up caddy-reverse-proxy -d - ``` diff --git a/docs/docs/configuration/env-variables.md b/docs/docs/configuration/env-variables.md index e7cd7a1fad..98208a2506 100644 --- a/docs/docs/configuration/env-variables.md +++ b/docs/docs/configuration/env-variables.md @@ -5,41 +5,47 @@ This page contains all available environment variables supported by Mathesar. Se ## Backend configuration {: #backend} -### `SECRET_KEY` +### `SECRET_KEY` {: #secret_key} -- **Description**: A unique random string used by Django for cryptographic signing ([see Django docs](https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-SECRET_KEY)). +- **Description**: A unique random string used by Django for cryptographic signing ([see Django docs](https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-SECRET_KEY)). It helps Mathesar secure user sessions and encrypt saved PostgreSQL passwords. - **Format**: A 50 character string -- **Additional information**: You can generate a secret key using [this tool](https://djecrety.ir/) if needed. +- **Additional information**: + To generate a secret key you can use [this browser-based generator](https://djecrety.ir/) or run this command on MacOS or Linux: -### `ALLOWED_HOSTS` + ``` + echo $(cat /dev/urandom | LC_CTYPE=C tr -dc 'a-zA-Z0-9' | head -c 50) + ``` -- **Description**: A list of hostnames that Mathesar will be accessible at ([see Django docs](https://docs.djangoproject.com/en/4.2/ref/settings/#allowed-hosts)). - - Hostnames should not contain the protocol (e.g. `http`) or trailing slashes. - - You can use `localhost` in this list. -- **Format**: Comma separated string of hostnames +## Internal Database configuration {: #db} - !!! success "Valid values" - - `mathesar.example.com, localhost` - - `.localhost, mathesar.example.com, 35.188.184.125` +!!!info + The database specified in this section will be used to store Mathesar's internal data. Additionally, it can be optionally repurposed via Mathesar's UI to store user data. - !!! failure "Invalid values" - - `http://mathesar.example.com/` - contains HTTP protocol and a trailing slash - - `https://mathesar.example.com` - contains HTTPS protocol - - `localhost/, 35.188.184.125` - contains trailing slash after `localhost` +### `POSTGRES_DB` -### `DJANGO_DATABASE_URL` +- **Description**: Specifies a name for the database that will be created and used by Mathesar for managing internal data. +- **Default value**: mathesar_django -- **Description**: A Postgres connection string of the database used for **Mathesar's internal usage**. -- **Format**:`postgres://user:password@hostname:port/database_name` - - The connection string above will connect to a database with username `user`, password `password`, hostname `mathesar_db`, port `5432`, and database name `mathesar_django`. +### `POSTGRES_USER` -### `MATHESAR_DATABASES` +- **Description**: Specifies creation of a user with superuser privileges and a database with the same name. +- **Default value**: mathesar -- **Description**: Names and connection information for databases managed by Mathesar. These databases will be accessible through the UI. -- **Format**:`(unique_id|connection_string),(unique_id|connection_string),...` - - e.g. `(db1|postgresql://u:p@example.com:5432/db1),(db2|postgresql://u:p@example.com:5432/db2)` - - This would set Mathesar to connect to two databases, `db1` and `db2` which are both accessed via the same user `u`, password `p`, hostname `example.com`, and port `5432`. +### `POSTGRES_PASSWORD` + +- **Description**: Specifies the superuser password that is required to be set for the PostgreSQL docker image. +- **Default value**: mathesar + +### `POSTGRES_HOST` + +- **Description**: Specifies the host name on which portgres listen for connections from client applications. +- **Default value**: mathesar_db + +### `POSTGRES_PORT` + +- **Description**: Specifies the port on which portgres listen for connections from client applications. +- **Default value**: 5432 ## Caddy reverse proxy configuration {: #caddy} @@ -61,26 +67,3 @@ This page contains all available environment variables supported by Mathesar. Se !!! tip "Tip" - Set this to `localhost` if you'd like Mathesar to be available only on localhost - Set the protocol to `http` if you don't want Caddy to automatically handle setting up SSL, e.g. `http://example.com` - - -### `HTTP_PORT` - -- **Description**: Configures the port that Caddy will use when `DOMAIN_NAME` specifies a `http` protocol. -- **Default value**: `80` - - !!! tip "Tip" - - It is recommended to use the default port `80` as features like automatic SSL rely on it ([see Caddy docs](https://caddyserver.com/docs/automatic-https#acme-challenges)). - - You probably want to change it to a different port if one of these is true: - - you already have a reverse proxy handling SSL on your system - - you are running Mathesar on a non-root system - -### `HTTPS_PORT` - -- **Description**: Configures the port that Caddy will use when `DOMAIN_NAME` specifies a `https` protocol or does not specify a protocol. -- **Default value**: `443` - - !!! tip "Tip" - - If you want Caddy to handle the SSL certificate it is highly recommended to use the default port `443` as features like automatic SSL, and HTTPS redirection rely on it ([see Caddy docs](https://caddyserver.com/docs/automatic-https#acme-challenges)). - - You probably want to change it to a different port if one of these is true: - - you already have a reverse proxy handling SSL on your system - - you are running Mathesar on a non-root system diff --git a/docs/docs/index.md b/docs/docs/index.md index 8a4d26d891..debb152435 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -1,32 +1,63 @@ # Mathesar Documentation - ## Welcome! -Mathesar is a self-hostable open source project that provides a spreadsheet-like interface to a PostgreSQL database. Our web-based interface helps you and your collaborators set up data models, edit data, and build custom reports – no technical skills needed. -You can create a new PostgreSQL database while setting up Mathesar or use our UI to interact with an existing database (or do both). +Mathesar is a self-hostable open source project that provides a spreadsheet-like interface to a PostgreSQL database. Our web-based interface helps you and your collaborators set up data models, edit data, and build custom reports — no technical skills needed. You can create a new PostgreSQL database while setting up Mathesar or use our UI to interact with an existing database (or do both). + +## Try Mathesar + +### Live demo + +See our [live demo site](https://demo.mathesar.org/) to try Mathesar without installing anything. + +### Try locally + +This is a quick way to play with Mathesar locally before installing, but will not be appropriate for saving data that you care about. + +1. With [Docker](https://docs.docker.com/get-docker/) installed, run: + + ``` + docker run -it --name mathesar -p 8000:8000 mathesar/mathesar-prod:latest + ``` + +1. Visit [http://localhost:8000/](http://localhost:8000/) to set up an admin user account and create a database connection. -A live demo of Mathesar is [available here](https://demo.mathesar.org/). + ??? tip "Tips when trying Mathesar locally" + - To open a [psql](https://www.postgresql.org/docs/current/app-psql.html) shell within the container, run: + + ``` + docker exec -it mathesar sudo -u postgres psql + ``` -## Installing Mathesar -You can self-host Mathesar by following one of the guides below: + - To stop Mathesar, press Ctrl+C in the shell where it is running. -- [Install with Docker Compose](installation/docker-compose/index.md) -- [Install from scratch (on Linux)](installation/build-from-source/index.md) -- [Install with our guided installation script](installation/guided-install/index.md) -- [Install using Docker image (needs an external database server)](installation/docker/index.md) + - To start again, run `docker start mathesar`. + + - To remove the Docker container, run `docker rm mathesar` . + + ⚠️ This will also delete the data that you've saved within Mathesar! + +## Install Mathesar + +You can self-host Mathesar by following one of the guides below: + +- [Install using Docker compose](installation/docker-compose/index.md) — a production setup with separate reverse-proxy and database containers. +- [Install from scratch](installation/build-from-source/index.md) — an advanced setup that doesn't rely on Docker. !!! info "More installation methods coming soon" We're working on supporting additional installation methods, and we'd appreciate feedback on which ones to prioritize. Please comment [on this issue](https://github.com/centerofci/mathesar/issues/2509) if you have thoughts. -## Using Mathesar +## Use Mathesar + Mathesar should be pretty intuitive to use. More documentation is coming soon, but for now, we've written some documentation for some things that could be tricky. - [Syncing Database Changes](./user-guide/syncing-db.md) - [Users & Access Levels](./user-guide/users.md) -## Contributing +## Contribute to Mathesar + As an open source project, we actively encourage contribution! Get started by reading our [Contributor Guide](https://github.com/centerofci/mathesar/blob/develop/CONTRIBUTING.md). ## Donate + We're a non-profit and your donations help sustain our core team. You can donate via [GitHub](https://github.com/sponsors/centerofci) or [Open Collective](https://opencollective.com/mathesar). diff --git a/docs/docs/installation/build-from-source/index.md b/docs/docs/installation/build-from-source/index.md index c221d31d7e..c9c542ed2e 100644 --- a/docs/docs/installation/build-from-source/index.md +++ b/docs/docs/installation/build-from-source/index.md @@ -3,35 +3,40 @@ !!! warning "For experienced Linux sysadmins" To follow this guide you need be experienced with Linux server administration, including the command line interface and some common utilities. + If you run into any trouble, we encourage you to [open an issue](https://github.com/mathesar-foundation/mathesar/issues/new/choose) or submit a PR proposing changes to [this file](https://github.com/mathesar-foundation/mathesar/blob/master/docs/docs/installation/build-from-source/index.md). ## Requirements ### System + We recommend having at least 60 GB disk space and 4 GB of RAM. ### Operating System -We've tested this on **Ubuntu**, but we expect that it can be adapted for other Linux distributions as well. + +We've tested this on **Debian 12**, but we expect that it can be adapted for other Linux distributions as well. ### Access + You should have **root access** to the machine you're installing Mathesar on. ### Software + You'll need to install the following system packages before you install Mathesar: -- [Python](https://www.python.org/downloads/) 3.9 +- [Python](https://www.python.org/downloads/) 3.9 or 3.10 !!! note "Python version" Python _older_ than 3.9 will not run Mathesar. - Python _newer_ than 3.9 will run Mathesar, but will require some slightly modified installation steps which we have [not yet documented](https://github.com/centerofci/mathesar/issues/2872). + Python _newer_ than 3.10 will run Mathesar, but will require some slightly modified installation steps which we have [not yet documented](https://github.com/centerofci/mathesar/issues/2872). -- [PostgreSQL](https://www.postgresql.org/download/linux/) 13 or newer (Verify with `psql --version`) +- [PostgreSQL](https://www.postgresql.org/download/linux/) 13 or newer (Verify by logging in, and running the query: `SELECT version();`) -- [NodeJS](https://nodejs.org/en/download) 14 or newer (Verify with `node --version`) +- [NodeJS](https://nodejs.org/en/download) 18 or newer (Verify with `node --version`) _(This is required for installation only and will eventually be [relaxed](https://github.com/centerofci/mathesar/issues/2871))_ -- [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 6 or newer (Verify with `npm --version`) +- [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) 9 or newer (Verify with `npm --version`) _(This is required for installation only and will eventually be [relaxed](https://github.com/centerofci/mathesar/issues/2871))_ @@ -59,29 +64,29 @@ Then press Enter to customize this guide with your domain name. 1. Open a `psql` shell. - ```sh - sudo -u postgres psql + ``` + sudo -u postgres psql # Modify based on your Postgres installation. ``` -1. Mathesar needs a Postgres superuser to function correctly. Let's create a superuser. +1. Let's create a Postgres user for Mathesar ```postgresql - CREATE USER mathesar WITH SUPERUSER ENCRYPTED PASSWORD '1234'; + CREATE USER mathesar WITH ENCRYPTED PASSWORD '1234'; ``` !!! warning "Customize your password" Be sure to change the password `1234` in the command above to something more secure and private. Record your custom password somewhere safe. You will need to reference it later. -1. Next, we have to create a database for storing Mathesar metadata. +1. Next, we have to create a database for storing Mathesar metadata. Your PostgreSQL user will either need to be a `SUPERUSER` or `OWNER` of the database. In this guide, we will be setting the user to be `OWNER` of the database as it is slightly restrictive compared to a `SUPERUSER`. ```postgresql - CREATE DATABASE mathesar_django; + CREATE DATABASE mathesar_django OWNER mathesar; ``` 1. Now we let us create a database for storing your data. ```postgresql - CREATE DATABASE your_db_name; + CREATE DATABASE your_db_name OWNER mathesar; ``` 1. Press Ctrl+D to exit the `psql` shell. @@ -131,7 +136,7 @@ Then press Enter to customize this guide with your domain name. 1. Clone the git repo into the installation directory. - ```sh + ``` git clone https://github.com/centerofci/mathesar.git . ``` @@ -149,14 +154,14 @@ Then press Enter to customize this guide with your domain name. 1. We need to create a python virtual environment for the Mathesar application. - ```sh + ``` -m venv ./mathesar-venv # /usr/bin/python3.9 -m venv ./mathesar-venv ``` 1. Next we will activate our virtual environment: - ```sh + ``` source ./mathesar-venv/bin/activate ``` @@ -168,39 +173,47 @@ Then press Enter to customize this guide with your domain name. 1. Install Python dependencies - ```sh - pip install -r requirements.txt + ``` + pip install -r requirements-prod.txt ``` 1. Set the environment variables - 1. Create .env file + 1. Create `.env` file - ```sh + ``` touch .env ``` - 1. Edit your `.env` file, making the following changes: - - - Add the [**Backend Configuration** environment variables](../../configuration/env-variables.md#backend) - - Customize the values of the environment variables to suit your needs. + 1. Edit your `.env` file, adding [environment variables](../../configuration/env-variables.md) to configure Mathesar. !!! example Your `.env` file should look something like this - ``` bash + ``` + DOMAIN_NAME='xDOMAIN_NAMEx' ALLOWED_HOSTS='xDOMAIN_NAMEx' - SECRET_KEY='dee551f449ce300ee457d339dcee9682eb1d6f96b8f28feda5283aaa1a21' - DJANGO_DATABASE_URL=postgresql://mathesar:1234@localhost:5432/mathesar_django - MATHESAR_DATABASES=(your_db_name|postgresql://mathesar:1234@localhost:5432/your_db_name) + SECRET_KEY='REPLACE_THIS_WITH_YOUR_RANDOMLY_GENERATED_VALUE' # REPLACE THIS! + POSTGRES_DB=mathesar_django + POSTGRES_USER=mathesar + POSTGRES_PASSWORD=mathesar1234 # Do not use this password! + POSTGRES_HOST=localhost + POSTGRES_PORT=5432 + ``` + + !!! tip + You can generate a [SECRET_KEY variable](../../configuration/env-variables.md#secret_key) by running: + + ``` + echo $(cat /dev/urandom | LC_CTYPE=C tr -dc 'a-zA-Z0-9' | head -c 50) ``` 1. Add the environment variables to the shell You need to `export` the environment variables listed in the `.env` file to your shell. The easiest way would be to run the below command. - ```sh - export $(sudo cat .env) + ``` + export $(cat .env) ``` !!! warning "Important" @@ -209,50 +222,50 @@ Then press Enter to customize this guide with your domain name. 1. Install the frontend dependencies - ```sh - npm install --prefix mathesar_ui + ``` + npm ci --prefix mathesar_ui ``` 1. Compile the Mathesar Frontend App - ```sh + ``` npm run --prefix mathesar_ui build --max_old_space_size=4096 ``` 1. Install Mathesar functions on the database: - ```sh - python install.py --skip-confirm | tee /tmp/install.py.log + ``` + python -m mathesar.install --skip-confirm | tee /tmp/install.py.log ``` 1. Create a media directory for storing user-uploaded media - ```sh + ``` mkdir .media ``` ### Set up Gunicorn -!!! info "" - We will use `systemd` to run the `gunicorn` service as it lets you use easily start and manage the service. +!!! note "Elevated permissions needed" + Most of the commands below need to be run as a root user, or using `sudo`. If you try to run one of these commands, and see an error about "permission denied", use one of those methods. 1. Create a user for running Gunicorn - ```sh - sudo groupadd gunicorn && \ - sudo useradd gunicorn -g gunicorn + ``` + groupadd gunicorn && \ + useradd gunicorn -g gunicorn ``` 1. Make the `gunicorn` user the owner of the `.media` directory - ```sh - sudo chown -R gunicorn:gunicorn .media/ + ``` + chown -R gunicorn:gunicorn .media/ ``` -1. Create the Gunicorn systemd service file. +1. Create the Gunicorn SystemD service file. - ```sh - sudo touch /lib/systemd/system/gunicorn.service + ``` + touch /lib/systemd/system/gunicorn.service ``` and copy the following code into it. @@ -276,34 +289,34 @@ Then press Enter to customize this guide with your domain name. WantedBy=multi-user.target ``` -1. Reload the systemctl and Start the Gunicorn socket +1. Reload `systemctl` and start the Gunicorn socket - ```sh - sudo systemctl daemon-reload && \ - sudo systemctl start gunicorn.service && \ - sudo systemctl enable gunicorn.service + ``` + systemctl daemon-reload + systemctl start gunicorn.service + systemctl enable gunicorn.service ``` 1. Check the logs to verify if Gunicorn is running without any errors - ```sh - sudo journalctl --priority=notice --unit=gunicorn.service + ``` + journalctl --priority=notice --unit=gunicorn.service ``` ### Set up the Caddy reverse proxy !!! info "" - We will be using the Caddy Reverse proxy to serve the static files and set up SSL certificates. + We will use the Caddy Reverse proxy to serve the static files and set up SSL certificates. 1. Create the CaddyFile - ```sh - sudo touch /etc/caddy/Caddyfile + ``` + touch /etc/caddy/Caddyfile ``` 2. Add the configuration details to the CaddyFile - ```text + ``` https://xDOMAIN_NAMEx { log { output stdout @@ -333,20 +346,20 @@ Then press Enter to customize this guide with your domain name. 1. Create a user for running Caddy - ```sh - sudo groupadd caddy && \ - sudo useradd caddy -g caddy + ``` + groupadd caddy && \ + useradd caddy -g caddy ``` 1. Create the Caddy systemd service file. - ```sh - sudo touch /lib/systemd/system/caddy.service + ``` + touch /lib/systemd/system/caddy.service ``` and copy the following code into it. - ```text + ``` [Unit] Description=Caddy Documentation=https://caddyserver.com/docs/ @@ -373,138 +386,19 @@ Then press Enter to customize this guide with your domain name. 1. Reload the systemctl and start the Caddy socket - ```sh - sudo systemctl daemon-reload && \ - sudo systemctl start caddy.service && \ - sudo systemctl enable caddy.service + ``` + systemctl daemon-reload && \ + systemctl start caddy.service && \ + systemctl enable caddy.service ``` 1. Check the logs to verify if Caddy is running without any errors - ```sh - sudo journalctl --priority=notice --unit=caddy.service + ``` + journalctl --priority=notice --unit=caddy.service ``` ### Set up your user account Mathesar is now installed! You can use it by visiting the URL `xDOMAIN_NAMEx`. -You'll be prompted to set up an admin user account the first time you open Mathesar. Just follow the instructions on screen. - -## Administration - -### Upgrade - -1. Go to your Mathesar installation directory. - - ```sh - cd xMATHESAR_INSTALLATION_DIRx - ``` - - !!! note - Your installation directory may be different from above if you used a different directory when installing Mathesar. - -1. Pull the latest version from the repository - - ```sh - git pull https://github.com/centerofci/mathesar.git - ``` - -1. Update Python dependencies - - ```sh - pip install -r requirements.txt - ``` - -1. Next we will activate our virtual environment: - - ```sh - source ./mathesar-venv/bin/activate - ``` - -1. Add the environment variables to the shell before running Django commands - - ```sh - export $(sudo cat .env) - ``` - -1. Run the latest Django migrations - - ```sh - python manage.py migrate - ``` - -1. Install the frontend dependencies - - ```sh - npm install --prefix mathesar_ui - ``` - -1. Build the Mathesar frontend app - - ```sh - npm run --prefix mathesar_ui build --max_old_space_size=4096 - ``` - -1. Update Mathesar functions on the database: - - ```sh - python install.py --skip-confirm >> /tmp/install.py.log - ``` - -1. Restart the gunicorn server - - ```sh - sudo systemctl restart gunicorn - ``` - - -### Uninstalling Mathesar {:#uninstall} - -1. Stop Caddy service - - ```sh - sudo systemctl disable caddy.service && sudo systemctl stop caddy.service - ``` - -1. Remove Caddy service file and Caddyfile - - ```sh - sudo rm /lib/systemd/system/caddy.service && sudo rm /etc/caddy/Caddyfile - ``` - -1. Stop Gunicorn - - ```sh - sudo systemctl disable gunicorn.service && sudo systemctl stop gunicorn.service - ``` - -1. Remove Gunicorn service file - - ```sh - sudo rm /lib/systemd/system/gunicorn.service - ``` - -1. Remove your Mathesar installation directory - - ```sh - sudo rm -r xMATHESAR_INSTALLATION_DIRx - ``` - - !!! warning "Your installation directory might be customized" - It's possible that Mathesar could have been installed into a different directory than shown above. Use caution when deleting this directory. - -1. Remove Django database - - 1. Connect to the psql terminal. - - ``` - sudo -u postgres psql - ``` - - 2. Drop the Django database. - - ```postgresql - DROP DATABASE mathesar_django; - ``` - -{% include 'snippets/uninstall-schemas.md' %} +You'll be prompted to set up an admin user account the first time you open Mathesar. Follow the instructions on screen. diff --git a/docs/docs/installation/docker-compose/index.md b/docs/docs/installation/docker-compose/index.md index 43f71a5ceb..50fbe07704 100644 --- a/docs/docs/installation/docker-compose/index.md +++ b/docs/docs/installation/docker-compose/index.md @@ -4,95 +4,100 @@ {% include 'snippets/docker-compose-prerequisites.md' %} -## Step-by-Step Guide {: #steps} - -1. Navigate to a directory where you'd like to store your Mathesar configuration. We recommend `/etc/mathesar`, but it can be any directory. - - ``` - sudo mkdir -p /etc/mathesar - cd /etc/mathesar - ``` -1. Download our [docker-compose.yml](https://github.com/centerofci/mathesar/raw/{{mathesar_version}}/docker-compose.yml), and [.env.example](https://github.com/centerofci/mathesar/raw/{{mathesar_version}}/.env.example) files to the directory you've chosen. +## Step-by-Step Guide {: #steps} - ``` - sudo wget https://github.com/centerofci/mathesar/raw/{{mathesar_version}}/docker-compose.yml - sudo wget https://github.com/centerofci/mathesar/raw/{{mathesar_version}}/.env.example - ``` +!!!note + Depending on your Docker setup, you may need to run `docker` commands with `sudo`. -1. Rename `.env.example` to `.env` +1. Download our [docker-compose.yml](https://github.com/mathesar-foundation/mathesar/raw/{{mathesar_version}}/docker-compose.yml) file. ``` - sudo mv .env.example .env + wget https://github.com/mathesar-foundation/mathesar/raw/{{mathesar_version}}/docker-compose.yml ``` - Your custom `.env` file will be used for setting [configuration variables](../../configuration/env-variables.md). +1. Open the downloaded docker-compose file using your text editor. -1. Set up the database - - To use the [default database server](../../configuration/customize-docker-compose#default-db) bundled with Mathesar, no additional steps are necessary. The database service will start along with the Mathesar web server. - - Alternatively, you can [disable the default database server](../../configuration/customize-docker-compose.md#disable-db-service) if you plan on using an [existing database server](../../configuration/connect-to-existing-db.md). +1. Set the required environment variables in the **x-config** section of the docker compose file. + + !!! Config + ```yaml + x-config: &config + # (REQUIRED) Replace '?' with '-' followed by a 50 character random string. + # You can generate one at https://djecrety.ir/ or by running: + # echo $(cat /dev/urandom | LC_CTYPE=C tr -dc 'a-zA-Z0-9' | head -c 50) + SECRET_KEY: ${SECRET_KEY:?} + + # (Optional) Replace 'http://localhost' with custom domain(s) e.g. + # 'yourdomain.com, 127.0.0.1' to manage the host(s) at which you want to + # access Mathesar over http or https + DOMAIN_NAME: ${DOMAIN_NAME:-http://localhost} + + # Edit the POSTGRES_* variables if you are not using the db service provided + # below, or if you want to use a custom database user. + + # (Optional) Replace 'mathesar_django' with any custom name for the internal + # database managed by mathesar web-service + POSTGRES_DB: ${POSTGRES_DB:-mathesar_django} + + # (Optional) Replace 'mathesar' with any custom username for the + # aforementioned database + POSTGRES_USER: ${POSTGRES_USER:-mathesar} + + # (Optional) Replace 'mathesar' with any custom password for the + # aforementioned database + + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mathesar} + # (Optional) Replace 'mathesar_db' with the name of the host running postgres + POSTGRES_HOST: ${POSTGRES_HOST:-mathesar_db} + + # (Optional) Replace '5432' with the port on which postgres is running + POSTGRES_PORT: ${POSTGRES_PORT:-5432} + ``` -1. Set up the web server. +1. Run the docker compose file using: + ``` + docker compose -f docker-compose.yml up + ``` - 1. Edit your `.env` file, making the following changes: +1. Set up your user account - - Add the [**Backend Configuration** environment variables](../../configuration/env-variables.md#backend) - - Customize the values of the environment variables to suit your needs. + Mathesar is now installed! You can use it by visiting `localhost` or the domain you've set up. You'll be prompted to set up an admin user account the first time you open Mathesar. Just follow the instructions on screen. - !!! example - If you are using the [default database container](../../configuration/customize-docker-compose#default-db), your `.env` file should look something like this - - ``` bash - ALLOWED_HOSTS='' - SECRET_KEY='dee551f449ce300ee457d339dcee9682eb1d6f96b8f28feda5283aaa1a21' - DJANGO_DATABASE_URL='postgresql://mathesar:mathesar@mathesar_db:5432/mathesar_django' - MATHESAR_DATABASES='(mathesar_tables|postgresql://mathesar:mathesar@mathesar_db:5432/mathesar)' - ``` +## Starting and stopping Mathesar {:#start-stop} - 1. Start the Mathesar web server. +The Mathesar server needs to be running for you to use Mathesar. If you restart your machine, you'll need to start the server again. - === "Linux" - ``` - sudo docker compose -f docker-compose.yml up service -d - ``` +- **Start** Mathesar: - === "MacOS" - ``` - docker compose -f docker-compose.yml up service -d - ``` + ``` + docker compose -f docker-compose.yml up -d + ``` -1. Set up the Caddy reverse proxy. + !!! Info + Exclude the `-d` flag if you'd like to see the container's logs. - 1. Edit your `.env` file, adding the [**Caddy Reverse Proxy** environment variables](../../configuration/env-variables.md#caddy). - - 1. Start the Caddy reverse proxy +- **Stop** Mathesar: - === "Linux" - ``` - sudo docker compose -f docker-compose.yml up caddy-reverse-proxy -d - ``` + ``` + docker compose -f docker-compose.yml down + ``` - === "MacOS" - ``` - docker compose -f docker-compose.yml up caddy-reverse-proxy -d - ``` + This stops all Mathesar Docker containers and releases their ports. -1. (Optional) Start the upgrade server to enable upgrading the docker image using the Mathesar UI. +## Optional configurations - === "Linux" - ``` - sudo docker compose -f docker-compose.yml up watchtower -d - ``` +### Hosting Mathesar over a custom domain with https - === "MacOS" - ``` - docker compose -f docker-compose.yml up watchtower -d - ``` +If you want Mathesar to be accessible over the internet, you'll probably want to set up a domain or sub-domain to use. **If you don't need a domain, you can skip this section.** -1. Set up your user account +**Ensure that the DNS for your domain or sub-domain is pointing to the public IP address of the machine that you're installing Mathesar on**. - Mathesar is now installed! You can use it by visiting `localhost` or the domain you've set up. +Add your domain(s) or sub-domain(s) to the [`DOMAIN_NAME`](../../configuration/env-variables/#domain_name) environment variable, in the **CONFIG** section of the docker-compose file. - You'll be prompted to set up an admin user account the first time you open Mathesar. Just follow the instructions on screen. +!!! example + ```yaml + DOMAIN_NAME: ${DOMAIN_NAME:-yourdomain.org, yoursubdomain.example.org} + ``` -{% include 'snippets/docker-compose-administration.md' %} +Restart the docker containers for the configuration to take effect. diff --git a/docs/docs/installation/docker/index.md b/docs/docs/installation/docker/index.md deleted file mode 100644 index 22362e6801..0000000000 --- a/docs/docs/installation/docker/index.md +++ /dev/null @@ -1,136 +0,0 @@ -# Install Mathesar web server via Docker - -Use our [official Docker image](https://hub.docker.com/r/mathesar/mathesar-prod/tags): `mathesar/mathesar-prod:latest` hosted on Docker Hub to run Mathesar. - -!!! warning "Limitations" - This installation procedure is intended for users who want to run a bare-bones version of the Mathesar web server. - - It is assumed you already have a database server and services like a reverse proxy typically needed for running a production setup. If you don't have those, please use the [Docker Compose installation documentation](../docker-compose/index.md). - - -## Prerequisites - -### Operating System -You can install Mathesar using this method on Linux, MacOS, and Windows. - -### Access -You should have permission to run Docker containers on the system. - -### Software -You'll need to install **[Docker](https://docs.docker.com/desktop/)** v23+ - -### Databases - -#### Database for Mathesar's internal usage -You'll need to: - -- Create a PostgreSQL database for Mathesar's internal usage. -- Create a database user for Mathesar to use. The user should be a `SUPERUSER`, [see PostgreSQL docs for more information](https://www.postgresql.org/docs/13/sql-createrole.html). -- Ensure that this database can accept network connections from the machine you're installing Mathesar on. -- Have the following information for this database handy before installation: - - Database hostname - - Database port - - Database name - - Database username - - Database password - -#### Databases connected to Mathesar's UI -Have the following information for all databases you'd like to connect to Mathesar's UI before installation: - -- Database hostname -- Database port -- Database name -- Database username (should be a `SUPERUSER`, see above) -- Database password - -!!! warning "Database creation" - Whenever the Docker container is started, we will attempt to create any databases in this list that don't already exist. So you don't need to ensure that they are created before installation. - -## Installation Steps - -1. Run the Mathesar Docker Image - - ```bash - docker run \ - --detach - -e DJANGO_DATABASE_URL='' \ - -e MATHESAR_DATABASES='(|)' \ - -e SECRET_KEY='' \ - -e ALLOWED_HOSTS='.localhost, 127.0.0.1, [::1]' \ - -v static:/code/static \ - -v media:/code/media \ - --name mathesar_service \ - -p 8000:8000 \ - --restart unless-stopped \ - mathesar/mathesar-prod:latest - ``` - - The above command creates a Docker container containing the Mathesar server running on the `localhost` and listening on port `8000`. It also: - - - Passes configuration options as environment variables to the Docker container. Refer to [Configuring Mathesar web server](../../configuration/env-variables.md#backend) for setting the correct value to these configuration options and for additional configuration options. The configuration options used in the above command are: - - `DJANGO_DATABASE_URL` - - `DJANGO_DATABASE_KEY` - - `MATHESAR_DATABASES` - - `SECRET_KEY` - - Creates two [named Docker volumes](https://docs.docker.com/storage/volumes/) - - `static` for storing static assets like CSS, js files - - `media` for storing user-uploaded media files - - Sets the container name as `mathesar_service` using the `--name` parameter, runs the container in a detached mode using the `--detach` parameter, and binds the port `8000` to the `localhost`. Refer to [Docker documentation](https://docs.docker.com/engine/reference/commandline/run/#options) for additional configuration options. - -1. Verify if the Mathesar server is running successfully: - ```bash - docker logs -f mathesar_service - ``` - -1. Set up your user account - - Mathesar is now installed! You can use it by visiting `localhost` or the domain you've set up. - - You'll be prompted to set up an admin user account the first time you open Mathesar. Just follow the instructions on screen. - -## Upgrading Mathesar {:#upgrade} - -1. Stop your existing Mathesar container: - - ```bash - docker stop mathesar_service - ``` - -1. Remove the old Mathesar Image - ```bash - docker rm mathesar_service - ``` - -1. Bump the image version in the `docker run` command you usually use to run your - Mathesar and start up a brand-new container: - - ```bash - docker run \ - -d \ - --name mathesar_service \ - # YOUR STANDARD ARGS HERE - mathesar/mathesar-prod:latest - ``` - -## Uninstalling Mathesar {:#uninstall} - -1. Remove the Mathesar container. - - ```bash - docker rm -v mathesar_service - ``` - -1. Remove the Mathesar Image - - ```bash - docker rmi mathesar_service - ``` - -1. Remove volumes related to Mathesar - - ```bash - docker volume rm static && - docker volume rm media - ``` - -{% include 'snippets/uninstall-schemas.md' %} diff --git a/docs/docs/installation/guided-install/index.md b/docs/docs/installation/guided-install/index.md deleted file mode 100644 index 32c9d235ac..0000000000 --- a/docs/docs/installation/guided-install/index.md +++ /dev/null @@ -1,44 +0,0 @@ -# Guided installation - -Our install script guides you through a series of prompts to install Mathesar. The script sets up Mathesar using Docker Compose [under the hood](./under-the-hood.md). - -!!! warning "Limitations" - This is a convenient way to install Mathesar. However, it's highly opinionated and requires `sudo` privileges (admin access), and only supports a limited set of configuration options. Use the [Docker Compose installation option](../docker-compose/) if you'd like to customize your installation. - -## Prerequisites - -{% include 'snippets/docker-compose-prerequisites.md' %} - - -## Overview - -The installation script will set up: - -- A Postgres database server to store data -- A web server to run the Mathesar application -- A reverse proxy server to serve static files and set up SSL certificates -- An upgrade server to handle upgrading Mathesar via the web interface - -If you'd like to know the steps performed by the install script in more detail, you can read our [Guided installation: under the hood](./under-the-hood.md) document. - -## Step-by-step guide {: #steps} - -!!! info "Getting help" - If you run into any problems during installation, see [our troubleshooting guide](./troubleshooting.md) or [open a ticket describing your problem](https://github.com/centerofci/mathesar/issues/new/choose). - -1. Paste this command into your terminal to begin installing the latest version of Mathesar: - - ```sh - bash <(curl -sfSL https://raw.githubusercontent.com/centerofci/mathesar/{{mathesar_version}}/install.sh) - ``` - -1. Follow the interactive prompts to configure your Mathesar installation. - -1. When finished, the installer will display the URL where you can run Mathesar from your web browser. - -!!! note "Connecting to additional databases" - Once you have successfully installed Mathesar, if you wish to connect an additional database to Mathesar, [instructions are here](../../configuration/connect-to-existing-db.md). - - - -{% include 'snippets/docker-compose-administration.md' %} diff --git a/docs/docs/installation/guided-install/troubleshooting.md b/docs/docs/installation/guided-install/troubleshooting.md deleted file mode 100644 index 061a9b797c..0000000000 --- a/docs/docs/installation/guided-install/troubleshooting.md +++ /dev/null @@ -1,64 +0,0 @@ -# Guided installation: troubleshooting - -!!! info "" - This document is related to our [guided installation](./index.md). - - -## Restarting installation - -If something has gone wrong with the installation, you may need to restart the script. Two cases are possible: - -1. The script has started the Docker environment for Mathesar. You can tell this happened if your terminal printed the message - ``` - Next, we'll download files and start the server, This may take a few minutes. - - Press ENTER to continue. - ``` - and you subsequently pressed `ENTER`. In this case, you must run the command - - === "Linux" - ```sh - sudo docker compose -f /etc/mathesar/docker-compose.yml down -v - ``` - - === "MacOS" - ```sh - docker compose -f /etc/mathesar/docker-compose.yml down -v - ``` - and then run the installation script again. - -2. The script hasn't yet started the Docker environment, i.e., you haven't seen the message printed above. In this case, you need only stop the script by pressing `CTRL+C`, and then run it again. - -## When installing on Windows {:#windows} - -!!! warning - The process of installing and running has thus far been much better tested on MacOS and Linux than it has on Windows. Please [open issues](https://github.com/centerofci/mathesar/issues/new/choose) for any Windows-specific problems you encounter. - -- During installation, choose "WSL 2" instead of "Hyper-V". WSL is the Windows Sub System for Linux and is required to run Mathesar. -- See [this tutorial](https://learn.microsoft.com/en-us/windows/wsl/tutorials/wsl-containers) for hints if you're having trouble getting Docker wired up properly. -- Make sure you're use the WSL command prompt (rather than DOS or PowerShell) when running the installation script and other commands. - -## Docker versions - -The most common problem we've encountered is users with out-of-date `docker` or `docker-compose` versions. - -- To determine your `docker-compose` version, run `docker compose version`. (Note the lack of hyphen.) You need `docker-compose` version 2.7 or higher for the installation to succeed. Better if it's version 2.10 or higher. -- To determine your `docker` version, run `docker --version`. We've tested with `docker` version 23, but lower versions may work. - -If you run `docker-compose --version` and see a relatively old version, try `docker compose version` and see whether it's different. The latter is the version that will be used in the script. - -## Ports - -You may see errors about various ports being unavailable (or already being bound). In this case, [uninstall Mathesar](./index.md#uninstall) to restart from a clean `docker` state, and choose non-default ports during the installation process for PostgreSQL, HTTP traffic, or HTTPS traffic as appropriate, e.g., using `8080` for HTTP traffic if `80` is unavailable. Note that if you customized the configuration directory, you must replace `/etc/mathesar` with that custom directory in the command. - -## Connection problems - -In order for Mathesar to install properly, it needs to download some artifacts from `https://raw.githubusercontent.com`. We've received some reports that this domain is blocked for some internet providers in India. If this is the case for you, consider routing around that problem via a custom DNS server, or using a VPN. - -## Permissions - -If you have permissions issues when the script begins running `docker` commands, double-check that your user is in the `sudoers` file. Try running `sudo -v`. If that gives an error, your user lacks needed permissions and you should speak with the administrator of your system. - -## Getting more help - -If you're having an issue not covered by this documentation, open an issue [on GitHub](https://github.com/centerofci/mathesar/issues). diff --git a/docs/docs/installation/guided-install/under-the-hood.md b/docs/docs/installation/guided-install/under-the-hood.md deleted file mode 100644 index 461b946d8c..0000000000 --- a/docs/docs/installation/guided-install/under-the-hood.md +++ /dev/null @@ -1,75 +0,0 @@ -# Guided installation: under the hood - -!!! info "" - This document is related to our [guided installation](./index.md). - -## What does the script do? -Our guided installation script performs the following steps. - -### Operating system check -The installer attempts to determine what operating system you're installing Mathesar on. We've tested with some variants of macOS as well as a few distros of Linux. Some logic in the installer branches based on your operating system. - -### Docker version check -The installer double-checks your Docker and Docker Compose versions, making sure that `docker` is at least version 20.0.0, and `docker-compose` is at least version 2.10.0. - -### Database configuration -Mathesar uses two PostgreSQL databases: - -- **an internal database**, used to store Mathesar related metadata such as display options. This is set up on the same machine as Mathesar's deployment. -- **the user database**, which stores your data. You can either set up a new database from scratch for this purpose or connect an existing PostgreSQL database. - -If you're setting a database up from scratch, the installer will set up credentials for both databases (a username and password), and also lets you customize the name of your user database. - -If you're connecting an existing database, you'll enter pre-existing credentials for the user database and set up new credentials for the Mathesar internal database. - -The credentials created in this section are used to log in directly to the database (i.e., not the Mathesar UI). You'll set up login credentials for the UI in a later step. - -Finally, Mathesar helps you customize the port exposed to your host machine from the database container. This is useful, since you'll need to have an exposed port to login to the Mathesar database(s) using an alternate client such as `psql`, but there could be a conflict on the default port (e.g. for the case that a PostgreSQL instance is running in the host OS). - -### Webserver configuration -This section lets you configure the entrypoint (Caddy) for every request you send to Mathesar. You may need to customize the ports if you have other services using ports on your machine. Additionally, you need to configure either a domain or an external IP address if you plan to expose your installation of Mathesar to the internet. Setting up the domain also gets HTTPS working properly on your Mathesar installation. - -#### Domain setup -The domain specified here should be a valid, registered domain, whose DNS entry points to the IP address of the server on which Mathesar is installed. DNS configuration should be done ahead of time. - -If you don't use a domain, Mathesar can still be accessed from the internet using an IP address instead a domain name. Please note that HTTPS will not work without a domain name set up. - -### Admin user configuration - -Here, the installer helps you create a user (separate from the database user) that you will use to login to Mathesar through the main web UI. This section walks you through that process to create a username and password for that user. You're allowed to use the same details as the database user above, but it's not required, or particularly recommended. The user created in this section will have admin privileges in Mathesar, and be able to create other users after the installation is complete. - -### Configuration directory - -We need to store all the details configured above, and we do so in a file in your configuration directory. Note that this contains your passwords and other secrets, so **it should be kept secure**. By default, this directory is `/etc/mathesar/`, but you can change it. - -We'll store two files under that directory: - -- `.env`: This file has the above-mentioned configurations. -- `docker-compose.yml` This is a config file downloaded from Mathesar's git repo. It defines the different Docker containers used by Mathesar, and how they're networked together. - -Recommended permissions for the `.env` file are: - -`-rw------- 1 root root 449 Feb 22 13:39 /etc/mathesar/.env` - -### Docker setup - -- The installer downloads the `docker-compose.yml` file from Mathesar's repo. -- The installer pulls all Docker images needed for Mathesar to run properly, and starts the various services in sequence. - -### Final steps - -If everything has worked, then the installer prints a message letting you know that it's succeeded, and gives a little information about where you should go to login to Mathesar. - -## Docker containers created -This installation process creates the following containers: - -- `mathesar_service`, which runs the main Mathesar application. -- `mathesar_db`, which runs the database (PostgreSQL 13). -- `mathesar-caddy-reverse-proxy-1`, which helps route traffic to the `mathesar_service` container. -- `mathesar-watchtower-1`, which helps upgrade Mathesar installation when new releases are available. - -## Files involved -This installation process creates the following files in the Mathesar configuration directory: - -- `.env`. This file defines the environment inside of the various Mathesar `docker` containers. It should be kept safe, since it has sensitive information about the passwords you set for Mathesar. If you've forgotten your admin username or password, look at this file. -- `docker-compose.yml`. This is the main file defining the Mathesar containers listed above, and the connections between them. diff --git a/docs/docs/releases/.gitignore b/docs/docs/releases/.gitignore new file mode 100644 index 0000000000..c431981551 --- /dev/null +++ b/docs/docs/releases/.gitignore @@ -0,0 +1,2 @@ +cache/*** +missing_prs.csv diff --git a/docs/docs/releases/0.1.0.md b/docs/docs/releases/0.1.0.md new file mode 100644 index 0000000000..6da412a43c --- /dev/null +++ b/docs/docs/releases/0.1.0.md @@ -0,0 +1,16 @@ +# Mathesar 0.1.0 (alpha release) + +Mathesar's first alpha release! Features: + +- **Built on Postgres**: Connect to an existing Postgres database or set one up from scratch. +- **Set up your data models**: Easily create and update Postgres schemas and tables. +- **Data entry**: Use our spreadsheet-like interface to view, create, update, and delete table records. +- **Filter, sort, and group**: Quickly slice your data in different ways. +- **Query builder**: Use our Data Explorer to build queries without knowing anything about SQL or joins. +- **Schema migrations**: Transfer columns between tables in two clicks. +- **Uses Postgres features**: Mathesar uses and manipulates Postgres schemas, primary keys, foreign keys, constraints and data types. e.g. "Links" in the UI are foreign keys in the database. +- **Custom data types**: Custom data types for emails and URLs (more coming soon), validated at the database level. +- **Basic access control**: Users can have Viewer (read-only), Editor (can only edit data, but not data structure), or Manager (can edit both data and its structure) roles. +- **Basic documentation**: Users can install Mathesar using Docker Compose, and tricky product features are documented. + +[Full Changelog](https://github.com/centerofci/mathesar/commits/0.1.0) diff --git a/docs/docs/releases/0.1.1.md b/docs/docs/releases/0.1.1.md new file mode 100644 index 0000000000..1ab7ad6d13 --- /dev/null +++ b/docs/docs/releases/0.1.1.md @@ -0,0 +1,24 @@ +# Mathesar 0.1.1 (alpha release) + +This is a minor release focused on addressing bugs and improving user experience. + +## Bug fixes + +- The UI now supports non-ASCII characters in column names and column settings. +- The record page works when the primary key is not an integer. +- Mathesar can now support primary keys that are UUIDs. +- Access level permissions presented on the UI are now consistent with the API access levels. +- Deleting newly created records immediately no longer results in getting stuck in a loading screen. +- Empty columns are now inferred as text instead of boolean during import. +- The UI now displays an appropriate failure message when failing to delete rows. +- Mathesar no longer crashes when attempting to order rows by non-orderable columns. +- Row selection gets cleared correctly when a placeholder cell is selected. + +## Improvements + +- The UI cancels edits when users press the Esc key in table cells. +- Group headers with record summaries now have links to allow users to navigate to the associated record. +- Dropdown positioning is improved across the app, so that they do not overflow the browser window. +- A bunch of smaller visual and UX improvements made by our Google Summer of Code (GSoC) applicants. + +[Full Changelog](https://github.com/centerofci/mathesar/compare/0.1.0...0.1.1) diff --git a/docs/docs/releases/0.1.2.md b/docs/docs/releases/0.1.2.md new file mode 100644 index 0000000000..07349696ba --- /dev/null +++ b/docs/docs/releases/0.1.2.md @@ -0,0 +1,42 @@ +# Mathesar 0.1.2 (alpha release) + +This release focuses on documenting additional options for installing Mathesar, some improvements to the user experience, and some bug fixes. We've also added support for switching between multiple databases in the UI. + +## Improvements to the UI + +- Mathesar now supports switching between multiple databases using the UI. _([#2847](https://github.com/mathesar-foundation/mathesar/pull/2847))_ +- You can now copy data from the Mathesar UI to paste into other applications. _([#2773](https://github.com/mathesar-foundation/mathesar/pull/2773))_ +- The first non-primary key column is now highlighted when a new record is created. _([#2515](https://github.com/mathesar-foundation/mathesar/pull/2515))_ +- Form inputs are disabled when the form is being submitted. _([#2762](https://github.com/mathesar-foundation/mathesar/pull/2762))_ +- Action pane sidebars are now resizable. _([#2808](https://github.com/mathesar-foundation/mathesar/pull/2808))_ +- Table deletion now requires you to enter the table's name (to prevent accidental deletion). _([#2858](https://github.com/mathesar-foundation/mathesar/pull/2858))_ +- Long table names are now truncated and the full name is shown on hover. _([#2825](https://github.com/mathesar-foundation/mathesar/pull/2825))_ +- We've disabled setting columns to JSON List and Map types using the UI until we have a better editing experience for cells of those types. _([#2772](https://github.com/mathesar-foundation/mathesar/pull/2772))_ +- Filter conditions can now be added and removed via the column header menu _([#2782](https://github.com/mathesar-foundation/mathesar/pull/2782))_ +- Cell level context menus now also show menu items related to the row and column. _([#2803](https://github.com/mathesar-foundation/mathesar/pull/2803))_ + +## Improvements to installation + +- We have documented additional installation options for Mathesar. Visit the [Mathesar docs site](https://docs.mathesar.org/) to explore these options. _([#2809](https://github.com/mathesar-foundation/mathesar/pull/2809) [#2826](https://github.com/mathesar-foundation/mathesar/pull/2826) [#2824](https://github.com/mathesar-foundation/mathesar/pull/2824))_ +- A reference for Mathesar configuration options has been added to our documentation. _([#2824](https://github.com/mathesar-foundation/mathesar/pull/2824))_ +- We have documented connecting to databases running on `localhost` outside of Docker. _([#2819](https://github.com/mathesar-foundation/mathesar/pull/2819))_ +- The Mathesar Docker image is now standalone and can be started using the `docker run` command. _([#2848](https://github.com/mathesar-foundation/mathesar/pull/2848))_ +- Superuser and database passwords are now validated when using the guided install script. _([#2625](https://github.com/mathesar-foundation/mathesar/pull/2625))_ + +## Bug fixes + +- Mathesar no longer crashes when importing tables with long column names. _([#2725](https://github.com/mathesar-foundation/mathesar/pull/2725))_ +- Static default values can no longer be assigned to a dynamic default column. _([#2780](https://github.com/mathesar-foundation/mathesar/pull/2780))_ +- Column names no longer overlap when the browser window is resized. _([#2856](https://github.com/mathesar-foundation/mathesar/pull/2856))_ +- Databases removed from the configuration environment file won't show up in the UI anymore. _([#2891](https://github.com/mathesar-foundation/mathesar/pull/2891))_ +- Fixed inconsistencies with the foreign key column icon. _([#2768](https://github.com/mathesar-foundation/mathesar/pull/2768))_ + +## API changes + +- The URL for the database page has been moved from `//` to `/db//` to avoid conflicts with other Mathesar URLs. _([#2791](https://github.com/mathesar-foundation/mathesar/pull/2791))_ + +## Maintenance + +- A "sponsors" section has been added to the README. _([#2710](https://github.com/mathesar-foundation/mathesar/pull/2710))_ + +[Full Changelog](https://github.com/centerofci/mathesar/compare/0.1.1...0.1.2) diff --git a/docs/docs/releases/0.1.3.md b/docs/docs/releases/0.1.3.md new file mode 100644 index 0000000000..7218f790f2 --- /dev/null +++ b/docs/docs/releases/0.1.3.md @@ -0,0 +1,147 @@ +# Mathesar 0.1.3 (alpha release) + +This release: + +- makes improvements to the installation process, +- adds support for sharing tables and explorations publicly, +- begins a framework for internationalization and translation of UI elements, +- moves DDL (SQL) logic to DB-layer functions to increase performance and reduce complexity, +- Improves summarization behavior in the data explorer, +- Adds support for importing JSON and Excel files, +- fixes user-reported issues, +- improves developer experience, +- fixes numerous small backend issues, +- fixes numerous small frontend issues, +- improves the user documentation, and +- improves the API documentation. + +## What's Changed + +### Installation improvements + +- Add superuser creation page _([#3088](https://github.com/centerofci/mathesar/pull/3088))_ +- Create superuser page's stylings _([#3131](https://github.com/centerofci/mathesar/pull/3131))_ +- Remove the documented steps for creating a superuser from the command line _([#3134](https://github.com/centerofci/mathesar/pull/3134))_ + +### Sharing tables and explorations + +- Shareable links backend - Models, APIs, bypass auth for table requests _([#3092](https://github.com/centerofci/mathesar/pull/3092))_ +- Shareable links frontend - shared table consumer view _([#3093](https://github.com/centerofci/mathesar/pull/3093))_ +- Shared queries - Auth handling for query requests, frontend consumer view, API tests _([#3113](https://github.com/centerofci/mathesar/pull/3113))_ +- UI for creating & managing shares for tables and explorations _([#3127](https://github.com/centerofci/mathesar/pull/3127))_ +- Shares - regenerate link, general fixes _([#3133](https://github.com/centerofci/mathesar/pull/3133))_ + +### Internationalization + +- Install typesafe-i18n & translates one component _([#3099](https://github.com/centerofci/mathesar/pull/3099))_ +- RichText component _([#3100](https://github.com/centerofci/mathesar/pull/3100))_ +- Django templates translatable _([#3101](https://github.com/centerofci/mathesar/pull/3101))_ + +### RSQLA1: Move DDL Operations to SQL functions + +- Sql test setup _([#2903](https://github.com/centerofci/mathesar/pull/2903))_ +- Add SQL for column adding _([#2923](https://github.com/centerofci/mathesar/pull/2923))_ +- Move constraint creation to SQL _([#2952](https://github.com/centerofci/mathesar/pull/2952))_ +- Cleaner consolidated logic for adding constraints _([#2976](https://github.com/centerofci/mathesar/pull/2976))_ +- Column creation and duplication DDL 2 _([#2978](https://github.com/centerofci/mathesar/pull/2978))_ +- SQL for links creation _([#2986](https://github.com/centerofci/mathesar/pull/2986))_ +- Table create ddl _([#3016](https://github.com/centerofci/mathesar/pull/3016))_ +- Add DDL functions for altering columns _([#3097](https://github.com/centerofci/mathesar/pull/3097))_ +- SQL tests for schema ddl _([#3098](https://github.com/centerofci/mathesar/pull/3098))_ +- Remove `pglast`, use SQL function instead _([#3107](https://github.com/centerofci/mathesar/pull/3107))_ +- Move table splitting logic to SQL _([#3119](https://github.com/centerofci/mathesar/pull/3119))_ +- Tests for links & constraints ddl _([#3120](https://github.com/centerofci/mathesar/pull/3120))_ +- Properly detect identity columns _([#3125](https://github.com/centerofci/mathesar/pull/3125))_ +- Wiring sql functions for links and tables _([#3130](https://github.com/centerofci/mathesar/pull/3130))_ +- Tests for alter table _([#3139](https://github.com/centerofci/mathesar/pull/3139))_ +- Add constraint copying to column extration logic _([#3168](https://github.com/centerofci/mathesar/pull/3168))_ + +### Summarization improvements + +- Fix SQL Syntax error while summarizing `Money, URI, Email` column _([#2911](https://github.com/centerofci/mathesar/pull/2911))_ +- Add `Sum` aggregation function _([#2893](https://github.com/centerofci/mathesar/pull/2893))_ +- Add `max` aggregation function _([#2912](https://github.com/centerofci/mathesar/pull/2912))_ +- Add `min` aggregation function _([#2914](https://github.com/centerofci/mathesar/pull/2914))_ +- Add `mean` aggregation function _([#2916](https://github.com/centerofci/mathesar/pull/2916))_ +- Add `median` aggregation function _([#2932](https://github.com/centerofci/mathesar/pull/2932))_ +- Add `Mode` aggregation function _([#2940](https://github.com/centerofci/mathesar/pull/2940))_ +- Add `Percentage True` aggregation function _([#2945](https://github.com/centerofci/mathesar/pull/2945))_ +- Add `Peak Time` aggregation function. _([#2981](https://github.com/centerofci/mathesar/pull/2981))_ +- Add `Peak Day of Week` aggregation function. _([#3004](https://github.com/centerofci/mathesar/pull/3004))_ +- Add `Peak Month` aggregation function. _([#3006](https://github.com/centerofci/mathesar/pull/3006))_ +- Fix `NaN:NaN` error while aggregating duration column _([#3136](https://github.com/centerofci/mathesar/pull/3136))_ + +### JSON and Excel file improvements + +- Updated datafile model to store file type _([#2890](https://github.com/centerofci/mathesar/pull/2890))_ +- Added methods to import a perfect JSON _([#2906](https://github.com/centerofci/mathesar/pull/2906))_ +- Removed code duplication while importing datafiles _([#2926](https://github.com/centerofci/mathesar/pull/2926))_ +- Added tests to check importing json feature _([#2933](https://github.com/centerofci/mathesar/pull/2933))_ +- Added pandas and JSON normalization code _([#2968](https://github.com/centerofci/mathesar/pull/2968))_ +- Added api tests for importing JSON feature _([#2977](https://github.com/centerofci/mathesar/pull/2977))_ +- Added documentation for importing data into tables _([#2992](https://github.com/centerofci/mathesar/pull/2992))_ +- Extended import via copy-paste for JSON and updated UI _([#3008](https://github.com/centerofci/mathesar/pull/3008))_ +- Updated documentation navigation to show importing data doc _([#3023](https://github.com/centerofci/mathesar/pull/3023))_ +- Added `max_level` param for JSON import feature in the backend _([#3039](https://github.com/centerofci/mathesar/pull/3039))_ +- Added functionality to import perfect Excel _([#3059](https://github.com/centerofci/mathesar/pull/3059))_ + +### Fixes for user-reported issues + +- Help text: "its linked tables" (possessive adjective) _([#3086](https://github.com/centerofci/mathesar/pull/3086))_ + +### DX improvements + +- Remove `.env` from developer guide. _([#2925](https://github.com/centerofci/mathesar/pull/2925))_ +- Add SQL files to the pytest workflow _([#3082](https://github.com/centerofci/mathesar/pull/3082))_ +- New linting rule _([#3116](https://github.com/centerofci/mathesar/pull/3116))_ +- Repeat failed tests _([#3118](https://github.com/centerofci/mathesar/pull/3118))_ +- Add pldebugger to dev db _([#3126](https://github.com/centerofci/mathesar/pull/3126))_ + +### Backend fixes and improvements + +- Fix migrations _([#2899](https://github.com/centerofci/mathesar/pull/2899))_ +- Remove lazydict dependency _([#2993](https://github.com/centerofci/mathesar/pull/2993))_ +- Add API tests for multi-column primary key constraints _([#3025](https://github.com/centerofci/mathesar/pull/3025))_ +- Support unknown types (backend) _([#3040](https://github.com/centerofci/mathesar/pull/3040))_ +- Allow usage of local.py for untracked settings _([#3064](https://github.com/centerofci/mathesar/pull/3064))_ +- Fix the error when `list aggregation` on mathesar custom array _([#3106](https://github.com/centerofci/mathesar/pull/3106))_ +- Merge db list demo mode commits into release 0.1.3 _([#3171](https://github.com/centerofci/mathesar/pull/3171))_ + +### Frontend fixes and improvements + +- Schema updates in database page without reloading. Fixes #2736 _([#2745](https://github.com/centerofci/mathesar/pull/2745))_ +- Make columns re-orderable _([#2831](https://github.com/centerofci/mathesar/pull/2831))_ +- Fix caret out of view when using Input on Chrome _([#2836](https://github.com/centerofci/mathesar/pull/2836))_ +- Improve TSV serialization when copying cells _([#2867](https://github.com/centerofci/mathesar/pull/2867))_ +- Add `max_split=1` to retrieve the column name _([#2956](https://github.com/centerofci/mathesar/pull/2956))_ +- Fix default value input stealing focus _([#2957](https://github.com/centerofci/mathesar/pull/2957))_ +- Auto-focus input when editing number/money cells _([#2975](https://github.com/centerofci/mathesar/pull/2975))_ +- Updated frontend to send a single bulk delete request instead of one request for each record _([#2985](https://github.com/centerofci/mathesar/pull/2985))_ +- Added margin between breadcrumb selector and bottom of the veiwport _([#3014](https://github.com/centerofci/mathesar/pull/3014))_ +- Date Input closes now on tab _([#3038](https://github.com/centerofci/mathesar/pull/3038))_ +- Scroll sheet all the way down when clicking the New Record button _([#3045](https://github.com/centerofci/mathesar/pull/3045))_ +- Use Truncate component in Record Selector table cells _([#3077](https://github.com/centerofci/mathesar/pull/3077))_ +- Copy formatted cell values to clipboard instead of raw values _([#3094](https://github.com/centerofci/mathesar/pull/3094))_ +- Fix regression: Move UserProfile to the App level context from Route level context _([#3175](https://github.com/centerofci/mathesar/pull/3175))_ + + +### Documentation + +- Update README.md with troubleshooting instructions _([#2751](https://github.com/centerofci/mathesar/pull/2751))_ +- Update documentation styles for active and hover _([#2937](https://github.com/centerofci/mathesar/pull/2937))_ +- Added the command that generates the API documentation schema file to… _([#2970](https://github.com/centerofci/mathesar/pull/2970))_ +- Added the command to copy the .env file, to the DEVELOPER GUIDE _([#2972](https://github.com/centerofci/mathesar/pull/2972))_ +- Update demo's documentation _([#2996](https://github.com/centerofci/mathesar/pull/2996))_ +- Fix typo error in DEVELOPER_GUIDE.md _([#2999](https://github.com/centerofci/mathesar/pull/2999))_ +- Update build from source documentation _([#3029](https://github.com/centerofci/mathesar/pull/3029))_ +- Clean up import docs _([#3042](https://github.com/centerofci/mathesar/pull/3042))_ + +### API documentation + +- Integrated drf-spectacular library _([#2939](https://github.com/centerofci/mathesar/pull/2939))_ +- Improved the operationIds by implementing a post hook function _([#3021](https://github.com/centerofci/mathesar/pull/3021))_ +- Added OpenAPI spec for datafiles endpoint _([#3044](https://github.com/centerofci/mathesar/pull/3044))_ +- Added OpenAPI specification for databases endpoint _([#3047](https://github.com/centerofci/mathesar/pull/3047))_ +- Added OpenAPI specification for /schemas/ endpoint _([#3074](https://github.com/centerofci/mathesar/pull/3074))_ + +[Full Changelog](https://github.com/centerofci/mathesar/compare/0.1.2...0.1.3) diff --git a/docs/docs/releases/0.1.4.md b/docs/docs/releases/0.1.4.md new file mode 100644 index 0000000000..9b8fa8d59b --- /dev/null +++ b/docs/docs/releases/0.1.4.md @@ -0,0 +1,138 @@ +# Mathesar 0.1.4 + +## Summary + +Mathesar 0.1.4 focuses on improving the installation and setup experience. + +## Upgrading to 0.1.4 + +See our guide on [upgrading Mathesar to 0.1.4](../administration/upgrade/0.1.4.md). + +## New Features + +### UI for configuring database connections + +Now you can add, edit, and delete connections to multiple databases from within Mathesar's UI. Previously this was only possible via editing text-based configuration. + +![image](https://github.com/mathesar-foundation/mathesar/assets/42411/2a51fe95-05bb-487a-bd54-283392039c56) + +![image](https://github.com/mathesar-foundation/mathesar/assets/42411/5a7916b7-4ab1-4b08-b7e3-a4823f3bcde5) + +_[#3170](https://github.com/mathesar-foundation/mathesar/pull/3170) [#3223](https://github.com/mathesar-foundation/mathesar/pull/3223) [#3299](https://github.com/mathesar-foundation/mathesar/pull/3299) [#3309](https://github.com/mathesar-foundation/mathesar/pull/3309) [#3319](https://github.com/mathesar-foundation/mathesar/pull/3319) [#3326](https://github.com/mathesar-foundation/mathesar/pull/3326) [#3341](https://github.com/mathesar-foundation/mathesar/pull/3341) [#3348](https://github.com/mathesar-foundation/mathesar/pull/3348) [#3349](https://github.com/mathesar-foundation/mathesar/pull/3349) [#3352](https://github.com/mathesar-foundation/mathesar/pull/3352) [#3354](https://github.com/mathesar-foundation/mathesar/pull/3354) [#3356](https://github.com/mathesar-foundation/mathesar/pull/3356) [#3368](https://github.com/mathesar-foundation/mathesar/pull/3368) [#3377](https://github.com/mathesar-foundation/mathesar/pull/3377) [#3387](https://github.com/mathesar-foundation/mathesar/pull/3387)_ + +### Sample data loader + +When adding a new database connection, you can choose to load sample data into that database. Sample data will be contained within specific schemas and may be useful to help new users play with Mathesar's features. + +![image](https://github.com/mathesar-foundation/mathesar/assets/42411/a7174f0d-254e-4463-9c74-3663deee91fa) + +_[#3368](https://github.com/mathesar-foundation/mathesar/pull/3368)_ + +### PostgreSQL column COMMENTs + +[PostgreSQL `COMMENT` values](https://www.postgresql.org/docs/current/sql-comment.html) on _columns_ are now exposed via a read/write "description" field within Mathesar. This feature was previously available for schemas and tables and is now available for columns too. + +![image](https://github.com/mathesar-foundation/mathesar/assets/42411/fd75136b-c577-47d0-9ab5-2b9418b980a5) + +_[#3186](https://github.com/mathesar-foundation/mathesar/pull/3186) [#3219](https://github.com/mathesar-foundation/mathesar/pull/3219)_ + +### Text-only imports + +When importing CSV data, Mathesar now gives you the option to use `TEXT` as the database type for all columns. This choice speeds up the import for larger data sets by skipping the process of guessing colum types. + +![image](https://github.com/mathesar-foundation/mathesar/assets/42411/6e0b5b1c-2e10-4e1f-8ad3-f4d99d28d8a9) + +_[#3050](https://github.com/mathesar-foundation/mathesar/pull/3050)_ + +We are still considering [additional ways](https://github.com/mathesar-foundation/mathesar/issues/2346) to improve performance when importing — especially for data sets with lots of columns. + +### Reduced database privilege installations + +Mathesar can now be installed as long as the database role used during the installation has at least `CONNECT` and `CREATE` privileges on the database targeted by the installation. If you want to create a new database for Mathesar's use, the installation will (naturally) require a role with the `CREATEDB` privilege. + +_[#3117](https://github.com/mathesar-foundation/mathesar/pull/3117)_ + +### Unified Mathesar Docker image + +The published Mathesar Docker image now contains a PostgreSQL server. This is used to provide a database backend in cases where Mathesar is started via Docker without being configured to connect to any other database. + +_[#3121](https://github.com/mathesar-foundation/mathesar/pull/3121) [#3212](https://github.com/mathesar-foundation/mathesar/pull/3212)_ + +### Metadata storage within SQLite + +We've added experimental SQLite support for the storage of Mathesar metadata. This will allow brave (or foolish) users to run Mathesar with this lighter-weight DB when installing from scratch on Linux. + +_[#3203](https://github.com/mathesar-foundation/mathesar/pull/3203) [#3225](https://github.com/mathesar-foundation/mathesar/pull/3225)_ _[#2778](https://github.com/mathesar-foundation/mathesar/pull/2778)_ + +### Improved PostgreSQL compatibility + +Mathesar now officially supports, and is tested against, Postgres versions 13, 14, and 15. It's also possible (but not yet recommended) to run Mathesar using Postgres 16. + +_[#3206](https://github.com/mathesar-foundation/mathesar/pull/3206)_ + +### Easier modification of sorting precedence + +When you have multiple sorting conditions applied to a table, you can now rearrange them via drag and drop to adjust the precedence of the sorting conditions. + +![image](https://github.com/mathesar-foundation/mathesar/assets/42411/5cb043db-0ebe-4664-961f-260873010e3b) + +_[#3316](https://github.com/mathesar-foundation/mathesar/pull/3316)_ + +### Cell values displayed within sidebar + +The table sidebar features a new "Cell" tab to show the content of cells, simplifying the process of viewing large text cells. + +![image](https://github.com/mathesar-foundation/mathesar/assets/42411/7dbad400-703a-4436-a494-1ccaf9928be6) + + +## Groundwork + +- We made significant progress towards internationalizing Mathesar's user interface. We expect to our next release to offer users the ability to toggle between English and Japanese. Subsequent releases will continue to add additional languages. + + _[#3102](https://github.com/mathesar-foundation/mathesar/pull/3102) [#3103](https://github.com/mathesar-foundation/mathesar/pull/3103) [#3104](https://github.com/mathesar-foundation/mathesar/pull/3104) [#3302](https://github.com/mathesar-foundation/mathesar/pull/3302) [#3321](https://github.com/mathesar-foundation/mathesar/pull/3321) [#3337](https://github.com/mathesar-foundation/mathesar/pull/3337) [#3340](https://github.com/mathesar-foundation/mathesar/pull/3340) [#3350](https://github.com/mathesar-foundation/mathesar/pull/3350) [#3389](https://github.com/mathesar-foundation/mathesar/pull/3389)_ + +- We began some work that will help us eventually distribute Mathesar via a Debian `.deb` package. Some [additional work](https://github.com/mathesar-foundation/mathesar/issues/2427) remains but we hope to introduce this installation method in a future version. + + _[#3189](https://github.com/mathesar-foundation/mathesar/pull/3189) [#3225](https://github.com/mathesar-foundation/mathesar/pull/3225)_ + +- We implemented the backend side of a new feature to import Excel and JSON files through Mathesar's import UI. More work still remains to implement the frontend side of this feature. + + _[#3083](https://github.com/mathesar-foundation/mathesar/pull/3083) [#3195](https://github.com/mathesar-foundation/mathesar/pull/3195) [#3132](https://github.com/mathesar-foundation/mathesar/pull/3132)_ + +- We took some baby steps towards building a system to automatically generate human-readable documentation for all our API endpoints. Significant work still remains. + + _[#3271](https://github.com/mathesar-foundation/mathesar/pull/3271) [#3146](https://github.com/mathesar-foundation/mathesar/pull/3146)_ + +## Bug fixes + +- Tables having `CHECK` constraints are now usable within Mathesar. _([#3243](https://github.com/mathesar-foundation/mathesar/pull/3243))_ +- Records can now be inserted into tables without primary keys. _([#3252](https://github.com/mathesar-foundation/mathesar/pull/3252))_ +- We fixed inconsistent state when selecting a different column while editing a column's name. _([#3219](https://github.com/mathesar-foundation/mathesar/pull/3225/3219))_ +- URL cells now retain their focus after a contained hyperlink is clicked. _([#3012](https://github.com/mathesar-foundation/mathesar/pull/3012))_ +- Searching for a record via a partially-entered date string no longer gives an error. _([#3343](https://github.com/mathesar-foundation/mathesar/pull/3343))_ +- The Database Page now shows loading and error indicators. _([#3351](https://github.com/mathesar-foundation/mathesar/pull/3351))_ +- The Schema Page now displays more detailed information about errors encountered when loading tables and explorations. _([#3323](https://github.com/mathesar-foundation/mathesar/pull/3323))_ +- Exclusion constraint violations now produce more helpful error messages. _([#3200](https://github.com/mathesar-foundation/mathesar/pull/3200))_ +- Files with missing or duplicate `id` values can now be imported without error. _([#3155](https://github.com/mathesar-foundation/mathesar/pull/3155))_ +- The record selector can now be closed by clicking on the overlay outside its modal. _([#3220](https://github.com/mathesar-foundation/mathesar/pull/3220))_ +- Help text for foreign key column data types is now more accurate. _([#3260](https://github.com/mathesar-foundation/mathesar/pull/3260))_ +- Users of [Mathesar's public demo site](https://demo.mathesar.org/) will no longer see database connections listed for other demo users. _([#3129](https://github.com/mathesar-foundation/mathesar/pull/3129))_ +- More UI elements have visually distinctive focus states. _([#3313](https://github.com/mathesar-foundation/mathesar/pull/3313))_ +- Date formatting is applied to arrays of date values. _([#3325](https://github.com/mathesar-foundation/mathesar/pull/3325))_ +- On the record page, values within foreign key columns can now be set to `NULL` more intuitively. _([#3310](https://github.com/mathesar-foundation/mathesar/pull/3310))_ +- A visual layout overflow bug on the record page is fixed. _([#3303](https://github.com/mathesar-foundation/mathesar/pull/3303))_ +- Foreign keys referencing non-primary-key columns now display properly. _([#3239](https://github.com/mathesar-foundation/mathesar/pull/3239))_ + +## Maintenance + +- We made our CI pipeline more robust. _([#3254](https://github.com/mathesar-foundation/mathesar/pull/3254))_ +- We made some updates to our workflows and developer documentation to support improvements to our issue labeling scheme. _([#3338](https://github.com/mathesar-foundation/mathesar/pull/3338) [#3298](https://github.com/mathesar-foundation/mathesar/pull/3298) [#3280](https://github.com/mathesar-foundation/mathesar/pull/3280) [#3336](https://github.com/mathesar-foundation/mathesar/pull/3336))_ +- We made some routine upgrades to dependencies and small adjustments to development tooling. _([#3214](https://github.com/mathesar-foundation/mathesar/pull/3214) [#3353](https://github.com/mathesar-foundation/mathesar/pull/3353) [#3334](https://github.com/mathesar-foundation/mathesar/pull/3334) [#3201](https://github.com/mathesar-foundation/mathesar/pull/3201) [#3295](https://github.com/mathesar-foundation/mathesar/pull/3295) [#3156](https://github.com/mathesar-foundation/mathesar/pull/3156) [#3234](https://github.com/mathesar-foundation/mathesar/pull/3234) [#3229](https://github.com/mathesar-foundation/mathesar/pull/3229) [#3317](https://github.com/mathesar-foundation/mathesar/pull/3317))_ +- We addressed regressions from work during this release. _([#3197](https://github.com/mathesar-foundation/mathesar/pull/3197))_ +- We improved error handling by preventing storing non-positive IDs for certain objects._([#3177](https://github.com/mathesar-foundation/mathesar/pull/3177))_ +- We clarified the API behavior by specifying JSON-only requests _([#3090](https://github.com/mathesar-foundation/mathesar/pull/3090))_ +- We improved testing against DB objects with long names _([#3140](https://github.com/mathesar-foundation/mathesar/pull/3140))_ +- We updated our org name to reflect a change from "Center of Complex Interventions" to "Mathesar Foundation". _([#3312](https://github.com/mathesar-foundation/mathesar/pull/3312))_ +- We made some improvements to our developer documentation. _([#3300](https://github.com/mathesar-foundation/mathesar/pull/3300) [#3210](https://github.com/mathesar-foundation/mathesar/pull/3210) [#3279](https://github.com/mathesar-foundation/mathesar/pull/3279))_ +- We resolved some merge conflicts after finalizing our previous release. _([#3190](https://github.com/mathesar-foundation/mathesar/pull/3190))_ + diff --git a/docs/docs/releases/README.md b/docs/docs/releases/README.md new file mode 100644 index 0000000000..faa4f7b7e5 --- /dev/null +++ b/docs/docs/releases/README.md @@ -0,0 +1,26 @@ +# Release notes + +This is developer documentation to help with release notes. It is not published in our docs guide. + +## How to generate release notes + +1. Create an empty release notes file. + + For example: + + ``` + touch 1.2.3.md + ``` + +1. Run this script to find PRs which have been merged but not yet included in the latest release notes file. + + ``` + ./find_missing_prs.sh + ``` + + (See comments within the script to better understand how it works.) + +1. Open `missing_prs.csv` to see the PRs you need to add. Incorporate them into the release notes as you see fit. Save the release notes and commit them. + +1. Re-run the script as needed. When more PRs are merged, they will appear in `missing_prs.csv`. + diff --git a/docs/docs/releases/find_missing_prs.sh b/docs/docs/releases/find_missing_prs.sh new file mode 100755 index 0000000000..612758b9ea --- /dev/null +++ b/docs/docs/releases/find_missing_prs.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +## DEPENDENCIES +## +## This script requires the following things to be installed: +## +## - The GitHub CLI: https://cli.github.com/ +## - DuckDB: https://duckdb.org/ + +COMMITS_FILE=cache/commits.txt +ALL_PRS_FILE=cache/all_prs.json +INCLUDED_PRS_FILE=cache/included_prs.txt +MISSING_PRS_FILE=missing_prs.csv + +# Use the latest release notes file +NOTES_FILE=$(ls -1 | grep -e '^[0-9]\.[0-9]\.[0-9]' | sort | tail -n 1) + +# Assume the release version number to match the file name +RELEASE=$(echo $NOTES_FILE | sed s/.md$//) + +# See all our local branches +BRANCHES=$(git branch --format="%(refname:short)") + +# Find the release branch. If we've already cut a branch for the release (and we +# have it locally), then use that. Otherwise, use "develop". +RELEASE_BRANCH=$( + if echo $BRANCHES | grep -q $RELEASE; then + echo $RELEASE + else + echo "develop" + fi +) + +# Find and cache the hashes for all the PR-merge commits included in the release +# branch but not included in the master branch. +git log --format=%H --first-parent master..$RELEASE_BRANCH > $COMMITS_FILE + +# Find and cache details about all the PRs merged within the past year. This +# gets more PRs than we need, but we'll filter it shortly. +gh pr list \ + --base $RELEASE_BRANCH \ + --limit 1000 \ + --json additions,author,deletions,mergeCommit,title,url \ + --search "is:closed merged:>$(date -d '1 year ago' '+%Y-%m-%d')" \ + --jq 'map({ + additions: .additions, + author: .author.login, + deletions: .deletions, + mergeCommit: .mergeCommit.oid, + title: .title, + url: .url + })' > $ALL_PRS_FILE + +# Find and cache the URLs to any PRs that we've already referenced in the +# release notes. +grep -Po 'https://github\.com/mathesar-foundation/mathesar/pull/\d*' \ + $NOTES_FILE > $INCLUDED_PRS_FILE + +# Generate a CSV containing details for PRs that match commits in the release +# but not in the release notes. +echo " + SELECT + pr.title, + pr.url, + pr.author, + pr.additions, + pr.deletions, + '[#' || regexp_extract(pr.url, '(\d+)$', 1) || '](' || pr.url || ')' AS link + FROM read_json('$ALL_PRS_FILE', auto_detect=true) AS pr + JOIN read_csv('$COMMITS_FILE', columns={'hash': 'text'}) AS commit + ON commit.hash = pr.mergeCommit + LEFT JOIN read_csv('$INCLUDED_PRS_FILE', columns={'url': 'text'}) AS included + ON included.url = pr.url + WHERE included.url IS NULL + ORDER BY pr.additions DESC;" | \ + duckdb -csv > $MISSING_PRS_FILE + diff --git a/docs/docs/snippets/docker-compose-administration.md b/docs/docs/snippets/docker-compose-administration.md deleted file mode 100644 index 382ab9456b..0000000000 --- a/docs/docs/snippets/docker-compose-administration.md +++ /dev/null @@ -1,86 +0,0 @@ -## Starting and stopping Mathesar {:#start-stop} - -The Mathesar server needs to be running for you to use Mathesar. If you restart your machine, you'll need to start the server again. - -- **Start** Mathesar: - - === "Linux" - ``` - sudo docker compose -f /etc/mathesar/docker-compose.yml up -d - ``` - - === "MacOS" - ``` - docker compose -f /etc/mathesar/docker-compose.yml up -d - ``` - -- **Stop** Mathesar: - - === "Linux" - ``` - sudo docker compose -f /etc/mathesar/docker-compose.yml down - ``` - - === "MacOS" - ``` - docker compose -f /etc/mathesar/docker-compose.yml down - ``` - - This stops all Mathesar Docker containers and releases their ports. - -!!! note - If you customized the Mathesar configuration directory during installation, you'll need to change `/etc/mathesar` to your configuration directory. - -## Upgrading Mathesar {:#upgrade} - -!!! tip "Upgrade from within Mathesar" - You can also run the upgrade from within Mathesar by logging in as an admin user and navigating to "Administration" (in the top right menu) > "Software Update" - -Manually upgrade Mathesar to the newest version using Watchtower: - -=== "Linux" - ``` - sudo docker exec mathesar-watchtower-1 /watchtower --run-once - ``` - -=== "MacOS" - ``` - docker exec mathesar-watchtower-1 /watchtower --run-once - ``` - -Manually upgrade Mathesar to the newest version without using Watchtower: - -=== "Linux" - ``` - sudo docker compose -f docker-compose.yml up --force-recreate --build service - ``` - -=== "MacOS" - ``` - docker compose -f docker-compose.yml up --force-recreate --build service - ``` - -## Uninstalling Mathesar {:#uninstall} - -1. Remove all Mathesar Docker images and containers. - - === "Linux" - ``` - sudo docker compose -f /etc/mathesar/docker-compose.yml down --rmi all -v - ``` - - === "MacOS" - ``` - docker compose -f /etc/mathesar/docker-compose.yml down --rmi all -v - ``` - -1. Remove configuration files. - - ```sh - sudo rm -rf /etc/mathesar - ``` - - !!! note - If you customized the Mathesar configuration directory during installation, you'll need to change `/etc/mathesar` to your configuration directory. - -{% include 'snippets/uninstall-schemas.md' %} \ No newline at end of file diff --git a/docs/docs/snippets/docker-compose-prerequisites.md b/docs/docs/snippets/docker-compose-prerequisites.md index f9947c9289..8add5e2a19 100644 --- a/docs/docs/snippets/docker-compose-prerequisites.md +++ b/docs/docs/snippets/docker-compose-prerequisites.md @@ -1,9 +1,6 @@ ### Operating System You can install Mathesar using this method on Linux, MacOS, and Windows. -### Access -You should have **root access** to the machine you're installing Mathesar on. - ### Software You'll need to install the following software before you install Mathesar: @@ -12,24 +9,3 @@ You'll need to install the following software before you install Mathesar: - **If you're installing on Windows:** - Ensure you have [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) installed - Turn on Docker Desktop WSL 2, [see Docker docs for more information](https://docs.docker.com/desktop/windows/wsl/#turn-on-docker-desktop-wsl-2) - -### Domain (optional) -If you want Mathesar to be accessible over the internet, you'll probably want to set up a domain or sub-domain to use. **If you don't need a domain, you can skip this section.** - -Before you start installation, **ensure that the DNS for your sub-domain or domain is pointing to the machine that you're installing Mathesar on**. - -### Database (optional) -You can create a new PostgreSQL database while setting up Mathesar or use our UI to interact with an existing database. **If you don't have a database you want to connect to, you can skip this section.** - -To connect Mathesar to an existing database: - -- The external database should be able to accept network connections from your Mathesar server. -- You'll need to set up a database user for Mathesar to use. The user should be a `SUPERUSER`, [see PostgreSQL docs for more information](https://www.postgresql.org/docs/13/sql-createrole.html). -- Have the following information handy before installation: - - Database hostname - - Database port - - Database name - - Database username - - Database password - -See _[Connect to an existing database](/configuration/connect-to-existing-db)_ for more details. diff --git a/docs/docs/user-guide/glossary.md b/docs/docs/user-guide/glossary.md new file mode 100644 index 0000000000..d88050bc98 --- /dev/null +++ b/docs/docs/user-guide/glossary.md @@ -0,0 +1,19 @@ +# Glossary + +## Internal database {:#internal-db} + +The "internal database" holds Mathesar-specific _metadata_ about the actual data (which lives in the [user database](#user-db)). Examples of such metadata include: + +- Exploration definitions +- Column display formatting settings +- Record summary template customizations +- Custom column ordering + +Each Mathesar installation requires one and only one internal database, and PostgreSQL and SQLite are both supported. + +## User database {:#user-db} + +The data you see within Mathesar lives in the "user database", which must use PostgreSQL. Each Mathesar installation can connect to multiple user databases, potentially on different servers. + +Mathesar also uses an [internal database](#internal-db) to store metadata. + diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 55bc284f85..1212ea5fa6 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -7,46 +7,40 @@ nav: - Introduction: - Welcome: index.md - Installation: - - Install with Docker Compose: installation/docker-compose/index.md + - Install using Docker Compose: installation/docker-compose/index.md - Install from scratch: installation/build-from-source/index.md - - Install with guided script: installation/guided-install/index.md - - Appendix to guided script: - - Under the hood: installation/guided-install/under-the-hood.md - - Troubleshooting: installation/guided-install/troubleshooting.md - - Install UI-only Docker image: installation/docker/index.md - Configuration: - Environment variables: configuration/env-variables.md - Connect to an existing database server: configuration/connect-to-existing-db.md - - Customize Docker Compose installations: configuration/customize-docker-compose.md - Administration: - - Upgrade Mathesar: administration/upgrade.md + - Upgrade: + - To 0.1.4: administration/upgrade/0.1.4.md + - To older versions: administration/upgrade/older.md - Uninstall Mathesar: administration/uninstall.md - Using Mathesar: - Introduction: user-guide/index.md - Importing data: user-guide/importing-data.md - Syncing database changes: user-guide/syncing-db.md - Users & access levels: user-guide/users.md + - Glossary: user-guide/glossary.md + - Releases: + - '0.1.4': releases/0.1.4.md + - '0.1.3': releases/0.1.3.md + - '0.1.2': releases/0.1.2.md + - '0.1.1': releases/0.1.1.md + - '0.1.0': releases/0.1.0.md plugins: - search: lang: en - redirects: redirect_maps: - "installation-dc/administration.md": "installation/guided-install/index.md#start-stop" "installation-dc/ansible-setup.md": "installation/docker-compose/index.md" - "installation-dc/quickstart.md": "installation/guided-install/index.md" - "installation-dc/troubleshooting.md": "installation/guided-install/troubleshooting.md" - "installation-dc/under-the-hood.md": "installation/guided-install/under-the-hood.md" - "installation-dc/uninstall.md": "installation/guided-install/index.md#uninstall" "product/intro.md": "user-guide/index.md" "product/syncing-db.md": "user-guide/syncing-db.md" "product/users.md": "user-guide/users.md" "install/index.md": "index.md" - "install/guided-install/index.md": "installation/guided-install/index.md" "install/docker-compose/index.md": "installation/docker-compose/index.md" - "install/docker-compose/under-the-hood.md": "installation/guided-install/under-the-hood.md" - "install/docker-compose/troubleshooting.md": "installation/guided-install/troubleshooting.md" - "install/docker/index.md": "installation/docker/index.md" "install/build-from-source/index.md": "installation/build-from-source/index.md" - macros - placeholder @@ -101,4 +95,4 @@ markdown_extensions: permalink: true extra: - mathesar_version: 0.1.3 + mathesar_version: 0.1.4 diff --git a/docs/placeholder-plugin.yaml b/docs/placeholder-plugin.yaml index e4312464d2..1e5161d101 100644 --- a/docs/placeholder-plugin.yaml +++ b/docs/placeholder-plugin.yaml @@ -1,2 +1,3 @@ MATHESAR_INSTALLATION_DIR: /etc/mathesar +MATHESAR_PG_DIR: /var/lib/docker/volumes/mathesar_postgresql_data/_data DOMAIN_NAME: localhost diff --git a/install.sh b/install.sh deleted file mode 100755 index 1fda25f5b9..0000000000 --- a/install.sh +++ /dev/null @@ -1,453 +0,0 @@ -#!/usr/bin/env bash -set -e -clear -x -github_tag=${1-"0.1.3"} -min_maj_docker_version=20 -min_maj_docker_compose_version=2 -min_min_docker_compose_version=7 -shopt -s expand_aliases - -## Functions ################################################################### - -percent_encode_reserved () { - # We need to be able to percent-encode any characters which are reserved in - # the URI spec given by RFC-3986, as well as '|', ' ', and '%' - # See https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 - local reserved='|:/?#[]@!$&'"'"'()*+,;=% ' - for (( i=0; i<${#1}; i++ )); do - local c="${1:$i:1}" - if [[ -z "${reserved##*"$c"*}" ]]; then - # $c is in the reserved set, convert to hex Note that the '02' in the - # formatting is not technically needed, since all reserved characters are - # greater than 10 (greater than 20, actually). We'll leave it this way to - # avoid potential future problems. - printf '%%%02X' "'${c}" - else - printf "%s" "${c}" - fi - done -} - -get_nonempty () { - local ret_str="${2}" - local prompt="${1}: " - if [ -n "${ret_str}" ]; then - prompt="${1} [${2}]: " - fi - read -r -p "${prompt}" - ret_str=${REPLY:-$ret_str} - until [ -n "${ret_str}" ]; do - read -r -p "This cannot be empty! -${prompt}" ret_str - done - echo "${ret_str}" -} - -get_password () { - local password - local prompt="${1}: " - local retry_prompt=" -The password cannot be empty! -${prompt}" - read -rs -p "${prompt}" password - until [ -n "${password}" ]; do - read -rs -p "${retry_prompt}" password - done - echo "${password}" -} -validate_password(){ - if [ ${#1} -lt 8 ]; then - return 1 - fi -} -create_password () { - local password - local password_check - local prompt="${1}Choose a password" - local repeat_prompt=" -Repeat the password: " - local repeat_retry=" -Passwords do not match! Try again. -" -local validation_prompt=" -Password must be at least 8 characters long! -" - password=$(get_password "${prompt}") - #check if the password is at least 8 characters long - - if validate_password "${password}"; then - read -rs -p "${repeat_prompt}" password_check - if [ "${password}" != "${password_check}" ]; then - password=$(create_password "${repeat_retry}") - fi - else - password=$(create_password "${validation_prompt}") - fi - echo "${password}" -} - -get_db_host () { - local prefix="${1}" - local db_host - db_host=$(get_nonempty "${prefix} database host") - while [ "${db_host:0:3}" == "127" ] || [ "${db_host}" == "localhost" ]; do - echo "Databases on localhost are not supported by this installation method." >&2 - db_host=$(get_nonempty "${prefix} database host") - done - echo "${db_host}" -} - -configure_db_urls () { - local default_db - local db_host - local db_port - local db_username - local db_password - local prefix - if [ "${1}" == preexisting ]; then - prefix="Enter the" - db_host=$(get_db_host "${prefix}") - enc_db_host=$(percent_encode_reserved "${db_host}") - else - prefix="Choose a" - default_db="mathesar" - enc_db_host=mathesar_db - fi - db_port=$(get_nonempty "${prefix} database connection port" "5432") - if [ "${1}" != django_only ]; then - db_name=$(get_nonempty "${prefix} database name" "${default_db}") - enc_db_name=$(percent_encode_reserved "${db_name}") - fi - if [ "${1}" != preexisting ] && [ "${1}" != django_only ]; then - printf " -Note: We will use the same user credentials across all databases created by Mathesar. - -" - fi - db_username=$(get_nonempty "${prefix} username for the database" "${default_db}") - enc_db_username=$(percent_encode_reserved "${db_username}") - if [ "${1}" == preexisting ]; then - db_password=$(get_password "${prefix} password") - else - db_password=$(create_password) - fi - enc_db_password=$(percent_encode_reserved "${db_password}") - - if [ "${1}" == preexisting ]; then - mathesar_database_url="postgresql://${enc_db_username}:${enc_db_password}@${enc_db_host}:${db_port}/${enc_db_name}" - elif [ "${1}" == django_only ]; then - django_database_url="postgresql://${enc_db_username}:${enc_db_password}@${enc_db_host}:5432/mathesar_django" - django_db_username="${db_username}" - django_db_password="${db_password}" - django_db_port="${db_port}" - else - mathesar_database_url="postgresql://${enc_db_username}:${enc_db_password}@${enc_db_host}:5432/${enc_db_name}" - django_database_url="postgresql://${enc_db_username}:${enc_db_password}@${enc_db_host}:5432/mathesar_django" - django_db_username="${db_username}" - django_db_password="${db_password}" - django_db_port="${db_port}" - fi -} - -################################################################################ - -printf " --------------------------------------------------------------------------------- - -Welcome to the Mathesar installer for version %s! - -For more information or explanation about the steps involved, please see: - -https://docs.mathesar.org/installation/guided-install/under-the-hood/ - --------------------------------------------------------------------------------- - -" "$github_tag" -read -r -p "Press ENTER to begin. " -clear -x - -printf " --------------------------------------------------------------------------------- - -OPERATING SYSTEM CHECK - --------------------------------------------------------------------------------- - -" -if [ "$(echo "${OSTYPE}" | head -c 5)" == "linux" ]; then - printf "Installing Mathesar for GNU/Linux. -" - alias docker='sudo docker' - INSTALL_OS='linux' -elif [ "$(echo "${OSTYPE}" | head -c 6)" == "darwin" ]; then - printf "Installing Mathesar for macOS. -" - INSTALL_OS='macos' -else - printf "Operating System Unknown. Proceed at your own risk. -" - alias docker='sudo docker' - INSTALL_OS='unknown' -fi -read -r -p " -Press ENTER to continue, or CTRL+C to cancel. " -clear -x - -installation_fail () { - printf " -Unfortunately, the installation has failed. - -We'll print some error logs above that will hopefully point you to the -problem. - -A common issue is for there to be some networking issue outside of Mathesar's -control. Please: -- Make sure you can reach your preexisting DB from this machine, if relevant. -- Make sure you have access to https://raw.githubusercontent.com/ - -If you can't get things working, please raise an issue at - -https://github.com/centerofci/mathesar/issues/ -" >&2 - - if [ "${1}" == "late" ]; then - read -r -p " - Press ENTER to print the logs and reset the local docker environment. " - docker compose logs - docker compose down -v --rmi all - fi - read -r -p " -Press ENTER to exit the installer. " - exit 1 -} - -printf " --------------------------------------------------------------------------------- - -DOCKER VERSION CHECK - -We'll begin by making sure your Docker installation is up-to-date. In order to -run some necessary commands, we need to use sudo for elevated privileges. - --------------------------------------------------------------------------------- - -" -sudo -k -sudo -v -docker_version=$(docker version -f '{{.Server.Version}}') -docker_compose_version=$(docker compose version --short) -printf " -Your Docker version is %s. -Your Docker Compose version is %s. " "$docker_version" "$docker_compose_version" -docker_maj_version=$(echo "$docker_version" | tr -d '[:alpha:]' | cut -d '.' -f 1) -docker_compose_maj_version=$(echo "$docker_compose_version" | tr -d '[:alpha:]' | cut -d '.' -f 1) -docker_compose_min_version=$(echo "$docker_compose_version" | tr -d '[:alpha:]' | cut -d '.' -f 2) - -if [ "$docker_maj_version" -lt "$min_maj_docker_version" ]; then - printf " -Docker must be at least version %s.0.0 and -Docker Compose must be at least version %s.%s.0! -Please upgrade. - -" "$min_maj_docker_version" "$min_maj_docker_compose_version" "$min_min_docker_compose_version" - exit 1 -fi - -if [ "$docker_compose_maj_version" -lt "$min_maj_docker_compose_version" ]; then - printf " -Docker Compose must be at least version %s.%s.0! Please upgrade. - -" "$min_maj_docker_compose_version" "$min_min_docker_compose_version" - exit 1 -elif [ "$docker_compose_maj_version" -eq "$min_maj_docker_compose_version" ] && -[ "$docker_compose_min_version" -lt "$min_min_docker_compose_version" ]; then - printf " -Docker Compose must be at least version %s.%s.0! Please upgrade. - -" "$min_maj_docker_compose_version" "$min_min_docker_compose_version" - exit 1 -fi - -printf " -Docker versions OK. - -" -read -r -p "Press ENTER to continue. " -clear -x - -printf " --------------------------------------------------------------------------------- - -DATABASE CONFIGURATION - -Here, we configure the PostgreSQL database(s) for Mathesar. These credentials -can be used to login directly using psql or another client. - --------------------------------------------------------------------------------- - -" - -printf " -Would you like to connect an existing database or create a new database? -" -select CHOICE in "connect existing" "create new"; do - case $CHOICE in - "connect existing") - printf " -WARNING: This will create a new PostgreSQL schema in the database for Mathesar's internal use. - -" - configure_db_urls preexisting - printf "\n" - printf " -Now we need to create a local database for Mathesar's internal use. -" - configure_db_urls django_only - break - ;; - "create new") - configure_db_urls - break - ;; - *) - printf "\nInvalid choice.\n" - esac -done -printf "\n" -clear -x -printf " --------------------------------------------------------------------------------- - -WEBSERVER CONFIGURATION - -Here, we configure the webserver that hosts Mathesar. - --------------------------------------------------------------------------------- - -" - -allowed_hosts=".localhost, 127.0.0.1" -read -r -p "Enter the domain name of the webserver, or press ENTER to skip: " domain_name -if [ -z "${domain_name}" ]; then - read -r -p "Enter the external IP address of the webserver, or press ENTER to skip: " ip_address - domain_name=':80' -fi -if [ -n "${ip_address}" ]; then - allowed_hosts="${ip_address}, ${allowed_hosts}" -elif [ "${domain_name}" != ':80' ]; then - allowed_hosts="${domain_name}, ${allowed_hosts}" -else - printf " -No domain or external IP address configured. -Only local connections will be allowed. - -" - read -r -p "Press ENTER to continue. " -fi -printf "\n" -read -r -p "Choose a HTTP port for the webserver to use [80]: " http_port -http_port=${http_port:-80} -read -r -p "Choose a HTTPS port for the webserver to use [443]: " https_port -https_port=${https_port:-443} -printf "Generating Django secret key... -" -secret_key=$(xxd -ps -c0 -l30 /dev/urandom) - -printf "\n" -clear -x -printf " --------------------------------------------------------------------------------- - -CONFIGURATION DIRECTORY - -Mathesar needs to create a configuration directory on your machine. Using the -default is strongly recommended. If you choose a custom location, write it down. - --------------------------------------------------------------------------------- - -" -read -r -p "Choose a configuration directory [/etc/mathesar]: " config_location -config_location="${config_location:-/etc/mathesar}" - -printf " -The environment file will be installed at %s/.env - -" "$config_location" - -read -r -p "Press ENTER to continue. " -sudo mkdir -p "${config_location}" -cd "${config_location}" -sudo tee .env > /dev/null < - # For sorting parameter formatting, see: - # https://github.com/centerofci/sqlalchemy-filters#sort-format def list(self, request, table_pk=None): paginator = TableLimitOffsetPagination() @@ -108,6 +106,16 @@ def list(self, request, table_pk=None): e, status_code=status.HTTP_400_BAD_REQUEST, ) + elif isinstance(e.orig, DatetimeFieldOverflow): + raise database_api_exceptions.InvalidDateAPIException( + e, + status_code=status.HTTP_400_BAD_REQUEST, + ) + else: + raise database_api_exceptions.MathesarAPIException( + e, + status_code=status.HTTP_400_BAD_REQUEST + ) serializer = RecordSerializer( records, @@ -145,17 +153,26 @@ def retrieve(self, request, pk=None, table_pk=None): def create(self, request, table_pk=None): table = get_table_or_404(table_pk) + primary_key_column_name = None + try: + primary_key_column_name = table.primary_key_column_name + except AssertionError: + raise generic_api_exceptions.MethodNotAllowedAPIException( + MethodNotAllowed, + error_code=ErrorCodes.MethodNotAllowed.value, + message="You cannot insert into tables without a primary key" + ) serializer = RecordSerializer(data=request.data, context=self.get_serializer_context(table)) serializer.is_valid(raise_exception=True) serializer.save() # TODO refactor to use serializer for more DRY response logic column_name_id_map = table.get_column_name_id_bidirectional_map() - table_pk_column_id = column_name_id_map[table.primary_key_column_name] + table_pk_column_id = column_name_id_map[primary_key_column_name] pk_value = serializer.data[table_pk_column_id] paginator = TableLimitOffsetPagination() record_filters = { "equal": [ - {"column_name": [table.primary_key_column_name]}, + {"column_name": [primary_key_column_name]}, {"literal": [pk_value]} ] } diff --git a/mathesar/api/db/viewsets/schemas.py b/mathesar/api/db/viewsets/schemas.py index 320e3c4b80..fb7dc64f24 100644 --- a/mathesar/api/db/viewsets/schemas.py +++ b/mathesar/api/db/viewsets/schemas.py @@ -24,15 +24,18 @@ class SchemaViewSet(AccessViewSetMixin, viewsets.GenericViewSet, ListModelMixin, def get_queryset(self): qs = Schema.objects.all().order_by('-created_at') + connection_id = self.request.query_params.get('connection_id') + if connection_id: + qs = qs.filter(database=connection_id) return self.access_policy.scope_viewset_queryset(self.request, qs) def create(self, request): serializer = SchemaSerializer(data=request.data, context={'request': request}) serializer.is_valid(raise_exception=True) - database_name = serializer.validated_data['database'].name + connection_id = serializer.validated_data['database'].id schema = create_schema_and_object( serializer.validated_data['name'], - database_name, + connection_id, comment=serializer.validated_data.get('description') ) serializer = SchemaSerializer(schema, context={'request': request}) diff --git a/mathesar/api/display_options.py b/mathesar/api/display_options.py index 514250968a..b4b872c58c 100644 --- a/mathesar/api/display_options.py +++ b/mathesar/api/display_options.py @@ -1,10 +1,17 @@ import json +from importlib import resources as impresources from mathesar.database.types import UIType +from mathesar import data def _money_display_options_schema(): - with open("currency_info.json", "r") as info_file: - currency_info = json.load(info_file) + try: + inp_file = (impresources.files(data) / 'currency_info.json') + with inp_file.open("rb") as f: # or "rt" as text file with universal newlines + currency_info = json.load(f) + except AttributeError: + # Python < PY3.9, fall back to method deprecated in PY3.11. + currency_info = json.load(impresources.open_text(data, 'currency_info.json')) currency_codes = list(currency_info.keys()) return { "options": [ diff --git a/mathesar/api/exceptions/database_exceptions/base_exceptions.py b/mathesar/api/exceptions/database_exceptions/base_exceptions.py index 5cdcb348f7..2432ef3d60 100644 --- a/mathesar/api/exceptions/database_exceptions/base_exceptions.py +++ b/mathesar/api/exceptions/database_exceptions/base_exceptions.py @@ -31,3 +31,19 @@ def __init__( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ): super().__init__(exception, error_code, message, field, details, status_code) + + +class InvalidDBConnection(MathesarAPIException): + # Default message is not needed as the exception string provides enough details + + def __init__( + self, + exception, + error_code=ErrorCodes.InvalidConnection.value, + message=None, + field=None, + details=None, + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ): + message = exception.args[0] + super().__init__(exception, error_code, message, field, details, status_code) diff --git a/mathesar/api/exceptions/database_exceptions/exceptions.py b/mathesar/api/exceptions/database_exceptions/exceptions.py index be5ca85490..a74727b764 100644 --- a/mathesar/api/exceptions/database_exceptions/exceptions.py +++ b/mathesar/api/exceptions/database_exceptions/exceptions.py @@ -400,11 +400,32 @@ class ExclusionViolationAPIException(MathesarAPIException): def __init__( self, exception, - message=None, + message="The requested update violates an exclusion constraint", field=None, details=None, + table=None, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR ): + if details is None and table is not None: + details = {} + try: + constraint_oid = get_constraint_oid_by_name_and_table_oid( + exception.orig.diag.constraint_name, + table.oid, + table._sa_engine + ) + constraint = Constraint.objects.get(table=table, oid=constraint_oid) + details = { + "constraint": constraint.id, + "constraint_columns": [c.id for c in constraint.columns], + } + except Exception: + warnings.warn("Could not enrich Exception") + details.update( + { + "original_details": exception.orig.diag.message_detail, + } + ) super().__init__(exception, self.error_code, message, field, details, status_code) diff --git a/mathesar/api/exceptions/error_codes.py b/mathesar/api/exceptions/error_codes.py index f08736b1e3..c5fd4749ce 100644 --- a/mathesar/api/exceptions/error_codes.py +++ b/mathesar/api/exceptions/error_codes.py @@ -16,6 +16,7 @@ class ErrorCodes(Enum): TypeError = 4102 ValueError = 4103 NetworkError = 4104 + InvalidConnection = 5001 # DB Error Codes CheckViolation = 4215 @@ -39,6 +40,7 @@ class ErrorCodes(Enum): UniqueImportViolation = 4303 # Validation Error + BadDBCredentials = 4428 ColumnSizeMismatch = 4401 DistinctColumnNameRequired = 4402 MappingsNotFound = 4417 @@ -49,6 +51,7 @@ class ErrorCodes(Enum): URLNotReachableError = 4405 URLInvalidContentType = 4406 UnknownDBType = 4408 + InvalidColumnOrder = 4430 InvalidDateError = 4413 InvalidDateFormatError = 4414 InvalidLinkChoice = 4409 @@ -67,3 +70,4 @@ class ErrorCodes(Enum): InvalidJSONFormat = 4425 UnsupportedJSONFormat = 4426 UnsupportedFileFormat = 4427 + UnsupportedInstallationDatabase = 4429 diff --git a/mathesar/api/exceptions/generic_exceptions/base_exceptions.py b/mathesar/api/exceptions/generic_exceptions/base_exceptions.py index cbc2000a34..3308f8a171 100644 --- a/mathesar/api/exceptions/generic_exceptions/base_exceptions.py +++ b/mathesar/api/exceptions/generic_exceptions/base_exceptions.py @@ -88,6 +88,22 @@ def __init__( self.status_code = status_code +class MethodNotAllowedAPIException(MathesarAPIException): + + def __init__( + self, + exception, + error_code=ErrorCodes.MethodNotAllowed.value, + message=None, + field=None, + details=None, + status_code=status.HTTP_405_METHOD_NOT_ALLOWED + ): + exception_detail = get_default_exception_detail(exception, error_code, message, field, details)._asdict() + self.detail = [exception_detail] + self.status_code = status_code + + class ValueAPIException(MathesarAPIException): # Default message is not needed as the exception string provides enough details @@ -116,3 +132,17 @@ def __init__( status_code=status.HTTP_503_SERVICE_UNAVAILABLE ): super().__init__(exception, error_code, message, field, details, status_code) + + +class BadDBCredentials(MathesarAPIException): + error_code = ErrorCodes.BadDBCredentials.value + + def __init__( + self, + exception, + field=None, + detail=None, + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ): + message = f"Bad credentials for connecting to the requested database. The reported error is {exception.args[0]}" + super().__init__(exception, self.error_code, message, field, detail, status_code) diff --git a/mathesar/api/exceptions/validation_exceptions/exceptions.py b/mathesar/api/exceptions/validation_exceptions/exceptions.py index c4a6f19a1b..b99e1e1f3b 100644 --- a/mathesar/api/exceptions/validation_exceptions/exceptions.py +++ b/mathesar/api/exceptions/validation_exceptions/exceptions.py @@ -148,6 +148,19 @@ def __init__( super().__init__(None, self.error_code, message, field, None) +class UnsupportedInstallationDatabase(MathesarValidationException): + error_code = ErrorCodes.UnsupportedInstallationDatabase.value + + def __init__( + self, + message=None, + field=None, + ): + if message is None: + message = "Installing on the internal database isn't allowed." + super().__init__(None, self.error_code, message, field, None) + + class InvalidTableName(MathesarValidationException): error_code = ErrorCodes.InvalidTableName.value @@ -181,3 +194,14 @@ def __init__( field=None, ): super().__init__(None, self.error_code, message, field) + + +class InvalidColumnOrder(MathesarValidationException): + error_code = ErrorCodes.InvalidColumnOrder.value + + def __init__( + self, + message="Invalid column order.", + field=None, + ): + super().__init__(None, self.error_code, message, field, None) diff --git a/mathesar/api/serializers/columns.py b/mathesar/api/serializers/columns.py index a1da3168a1..338d3c166e 100644 --- a/mathesar/api/serializers/columns.py +++ b/mathesar/api/serializers/columns.py @@ -146,10 +146,14 @@ class Meta(SimpleColumnSerializer.Meta): 'valid_target_types', 'default', 'has_dependents', + 'description', ) model_fields = (DISPLAY_OPTIONS_KEY,) name = serializers.CharField(required=False, allow_blank=True) + description = serializers.CharField( + required=False, allow_blank=True, default=None, allow_null=True + ) # From scratch fields type = serializers.CharField(required=False) diff --git a/mathesar/api/serializers/constraints.py b/mathesar/api/serializers/constraints.py index 761f211099..0dc9b8cde8 100644 --- a/mathesar/api/serializers/constraints.py +++ b/mathesar/api/serializers/constraints.py @@ -139,6 +139,10 @@ class Meta: 'foreignkey': ForeignKeyConstraintSerializer, 'primary': BaseConstraintSerializer, 'unique': BaseConstraintSerializer, + # Even though 'check' & 'exclude' constraints are currently unsupported it's added here + # so that the app doesn't break in case these constraints are already present. + 'check': BaseConstraintSerializer, + 'exclude': BaseConstraintSerializer } def get_mapping_field(self, data): @@ -165,7 +169,7 @@ def run_validation(self, data): field='referent_table' ) constraint_type = data.get('type', None) - if constraint_type not in self.serializers_mapping.keys(): + if constraint_type not in ('foreignkey', 'primary', 'unique'): raise UnsupportedConstraintAPIException(constraint_type=constraint_type) columns = data.get('columns', None) if columns == []: diff --git a/mathesar/api/serializers/data_files.py b/mathesar/api/serializers/data_files.py index 524d3c24ae..2c684b0776 100644 --- a/mathesar/api/serializers/data_files.py +++ b/mathesar/api/serializers/data_files.py @@ -22,7 +22,7 @@ class Meta: model = DataFile fields = [ 'id', 'file', 'table_imported_to', 'user', 'header', 'delimiter', - 'escapechar', 'quotechar', 'paste', 'url', 'created_from', 'max_level' + 'escapechar', 'quotechar', 'paste', 'url', 'created_from', 'max_level', 'sheet_index' ] extra_kwargs = { 'file': {'required': False}, diff --git a/mathesar/api/serializers/databases.py b/mathesar/api/serializers/databases.py index 9563ff418b..7911b909e7 100644 --- a/mathesar/api/serializers/databases.py +++ b/mathesar/api/serializers/databases.py @@ -6,19 +6,24 @@ from mathesar.models.base import Database -class DatabaseSerializer(MathesarErrorMessageMixin, serializers.ModelSerializer): +class ConnectionSerializer(MathesarErrorMessageMixin, serializers.ModelSerializer): supported_types_url = serializers.SerializerMethodField() + nickname = serializers.CharField(source='name') + database = serializers.CharField(source='db_name') class Meta: model = Database - fields = ['id', 'name', 'deleted', 'supported_types_url'] - read_only_fields = ['id', 'name', 'deleted', 'supported_types_url'] + fields = ['id', 'nickname', 'database', 'supported_types_url', 'username', 'password', 'host', 'port'] + read_only_fields = ['id', 'supported_types_url'] + extra_kwargs = { + 'password': {'write_only': True} + } def get_supported_types_url(self, obj): - if isinstance(obj, Database): + if isinstance(obj, Database) and not self.partial: # Only get records if we are serializing an existing table request = self.context['request'] - return request.build_absolute_uri(reverse('database-types', kwargs={'pk': obj.pk})) + return request.build_absolute_uri(reverse('connection-types', kwargs={'pk': obj.pk})) else: return None diff --git a/mathesar/api/serializers/records.py b/mathesar/api/serializers/records.py index f2fb5a311e..9367c5dff4 100644 --- a/mathesar/api/serializers/records.py +++ b/mathesar/api/serializers/records.py @@ -1,4 +1,4 @@ -from psycopg2.errors import NotNullViolation, UniqueViolation, CheckViolation +from psycopg2.errors import NotNullViolation, UniqueViolation, CheckViolation, ExclusionViolation from rest_framework import serializers from rest_framework import status from sqlalchemy.exc import IntegrityError @@ -53,6 +53,12 @@ def update(self, instance, validated_data): e, status_code=status.HTTP_400_BAD_REQUEST ) + elif type(e.orig) is ExclusionViolation: + raise database_api_exceptions.ExclusionViolationAPIException( + e, + table=table, + status_code=status.HTTP_400_BAD_REQUEST, + ) else: raise database_api_exceptions.MathesarAPIException(e, status_code=status.HTTP_400_BAD_REQUEST) return record @@ -80,6 +86,12 @@ def create(self, validated_data): e, status_code=status.HTTP_400_BAD_REQUEST ) + elif type(e.orig) is ExclusionViolation: + raise database_api_exceptions.ExclusionViolationAPIException( + e, + table=table, + status_code=status.HTTP_400_BAD_REQUEST, + ) else: raise database_api_exceptions.MathesarAPIException(e, status_code=status.HTTP_400_BAD_REQUEST) return record diff --git a/mathesar/api/serializers/schemas.py b/mathesar/api/serializers/schemas.py index 10c5de682a..f345362ed5 100644 --- a/mathesar/api/serializers/schemas.py +++ b/mathesar/api/serializers/schemas.py @@ -1,4 +1,4 @@ -from rest_access_policy import PermittedSlugRelatedField +from rest_access_policy import PermittedPkRelatedField from rest_framework import serializers from db.identifiers import is_identifier_too_long @@ -15,11 +15,10 @@ class SchemaSerializer(MathesarErrorMessageMixin, serializers.HyperlinkedModelSerializer): name = serializers.CharField() # Restrict access to databases with create access. - # Unlike PermittedPkRelatedField this field uses a slug instead of an id # Refer https://rsinger86.github.io/drf-access-policy/policy_reuse/ - database = PermittedSlugRelatedField( + connection_id = PermittedPkRelatedField( + source='database', access_policy=DatabaseAccessPolicy, - slug_field='name', queryset=Database.current_objects.all() ) description = serializers.CharField( @@ -31,7 +30,7 @@ class SchemaSerializer(MathesarErrorMessageMixin, serializers.HyperlinkedModelSe class Meta: model = Schema fields = [ - 'id', 'name', 'database', 'has_dependents', 'description', + 'id', 'name', 'connection_id', 'has_dependents', 'description', 'num_tables', 'num_queries' ] diff --git a/mathesar/api/serializers/table_settings.py b/mathesar/api/serializers/table_settings.py index 8e1b81a2d2..576fb0c569 100644 --- a/mathesar/api/serializers/table_settings.py +++ b/mathesar/api/serializers/table_settings.py @@ -1,8 +1,8 @@ from rest_framework import serializers from mathesar.api.exceptions.mixins import MathesarErrorMessageMixin - -from mathesar.models.base import PreviewColumnSettings, TableSettings, compute_default_preview_template +from mathesar.api.exceptions.validation_exceptions.exceptions import InvalidColumnOrder +from mathesar.models.base import PreviewColumnSettings, TableSettings, compute_default_preview_template, ValidationError class PreviewColumnSerializer(MathesarErrorMessageMixin, serializers.ModelSerializer): @@ -16,6 +16,7 @@ class Meta: class TableSettingsSerializer(MathesarErrorMessageMixin, serializers.HyperlinkedModelSerializer): preview_settings = PreviewColumnSerializer() + column_order = serializers.ListField(child=serializers.IntegerField()) class Meta: model = TableSettings @@ -36,6 +37,9 @@ def update(self, instance, validated_data): column_order_data = validated_data.pop('column_order', None) if column_order_data is not None: - instance.column_order = column_order_data - instance.save() + try: + instance.column_order = column_order_data + instance.save() + except ValidationError: + raise InvalidColumnOrder() return instance diff --git a/mathesar/api/ui/permissions/ui_database.py b/mathesar/api/ui/permissions/ui_database.py index 228aa76f9a..b1664635b8 100644 --- a/mathesar/api/ui/permissions/ui_database.py +++ b/mathesar/api/ui/permissions/ui_database.py @@ -11,9 +11,22 @@ class UIDatabaseAccessPolicy(AccessPolicy): """ statements = [ { - 'action': ['list', 'retrieve', 'types', 'functions', 'filters'], + 'action': [ + 'list', 'retrieve', 'types', 'functions', 'filters' + ], 'principal': 'authenticated', 'effect': 'allow', + }, + { + 'action': [ + 'create', 'partial_update', 'destroy', + 'create_from_known_connection', + 'create_from_scratch', + 'create_with_new_user', + ], + 'principal': 'authenticated', + 'effect': 'allow', + 'condition': 'is_superuser' } ] diff --git a/mathesar/api/ui/serializers/users.py b/mathesar/api/ui/serializers/users.py index 26fafd0cde..199c439ac4 100644 --- a/mathesar/api/ui/serializers/users.py +++ b/mathesar/api/ui/serializers/users.py @@ -40,6 +40,7 @@ class Meta: 'is_superuser', 'database_roles', 'schema_roles', + 'display_language' ] extra_kwargs = { 'password': {'write_only': True}, diff --git a/mathesar/api/ui/viewsets/__init__.py b/mathesar/api/ui/viewsets/__init__.py index 4532e86193..53da991a40 100644 --- a/mathesar/api/ui/viewsets/__init__.py +++ b/mathesar/api/ui/viewsets/__init__.py @@ -1,4 +1,4 @@ -from mathesar.api.ui.viewsets.databases import DatabaseViewSet # noqa +from mathesar.api.ui.viewsets.databases import ConnectionViewSet # noqa from mathesar.api.ui.viewsets.users import * # noqa from mathesar.api.ui.viewsets.version import VersionViewSet # noqa from mathesar.api.ui.viewsets.records import RecordViewSet # noqa diff --git a/mathesar/api/ui/viewsets/databases.py b/mathesar/api/ui/viewsets/databases.py index ad78abc609..0bdc1770cb 100644 --- a/mathesar/api/ui/viewsets/databases.py +++ b/mathesar/api/ui/viewsets/databases.py @@ -1,5 +1,7 @@ +from django.db.utils import IntegrityError from django_filters import rest_framework as filters -from rest_framework import viewsets +from rest_access_policy import AccessViewSetMixin +from rest_framework import serializers, status, viewsets from rest_framework.decorators import action from rest_framework.mixins import ListModelMixin, RetrieveModelMixin from rest_framework.response import Response @@ -7,16 +9,28 @@ from mathesar.api.ui.permissions.ui_database import UIDatabaseAccessPolicy from mathesar.models.base import Database from mathesar.api.dj_filters import DatabaseFilter +from mathesar.api.exceptions.validation_exceptions.exceptions import ( + DictHasBadKeys, UnsupportedInstallationDatabase +) +from mathesar.api.exceptions.database_exceptions.base_exceptions import IntegrityAPIException from mathesar.api.pagination import DefaultLimitOffsetPagination -from mathesar.api.serializers.databases import DatabaseSerializer, TypeSerializer +from mathesar.api.serializers.databases import ConnectionSerializer, TypeSerializer from mathesar.api.serializers.filters import FilterSerializer from mathesar.filters.base import get_available_filters +from mathesar.utils.connections import ( + copy_connection_from_preexisting, create_connection_from_scratch, + create_connection_with_new_user, BadInstallationTarget +) -class DatabaseViewSet(viewsets.GenericViewSet, ListModelMixin, RetrieveModelMixin): - serializer_class = DatabaseSerializer +class ConnectionViewSet( + AccessViewSetMixin, + ListModelMixin, RetrieveModelMixin, + viewsets.GenericViewSet, +): + serializer_class = ConnectionSerializer pagination_class = DefaultLimitOffsetPagination filter_backends = (filters.DjangoFilterBackend,) filterset_class = DatabaseFilter @@ -42,3 +56,80 @@ def filters(self, request, pk=None): available_filters = get_available_filters(engine) serializer = FilterSerializer(available_filters, many=True) return Response(serializer.data) + + @action(methods=['post'], detail=False, serializer_class=serializers.Serializer) + def create_from_known_connection(self, request): + try: + created_connection = copy_connection_from_preexisting( + request.data['credentials']['connection'], + request.data['nickname'], + request.data['database_name'], + request.data.get('create_database', False), + request.data.get('sample_data', []), + ) + except KeyError as e: + raise DictHasBadKeys( + field=e.args[0] + ) + except BadInstallationTarget: + raise UnsupportedInstallationDatabase() + except IntegrityError as e: + raise IntegrityAPIException(e, status_code=status.HTTP_400_BAD_REQUEST) + serializer = ConnectionSerializer( + created_connection, context={'request': request}, many=False + ) + return Response(serializer.data) + + @action(methods=['post'], detail=False, serializer_class=serializers.Serializer) + def create_from_scratch(self, request): + try: + credentials = request.data['credentials'] + created_connection = create_connection_from_scratch( + credentials['user'], + credentials['password'], + credentials['host'], + credentials['port'], + request.data['nickname'], + request.data['database_name'], + request.data.get('sample_data', []), + ) + except KeyError as e: + raise DictHasBadKeys( + message="Required key missing", + field=e.args[0] + ) + except BadInstallationTarget: + raise UnsupportedInstallationDatabase() + except IntegrityError as e: + raise IntegrityAPIException(e, status_code=status.HTTP_400_BAD_REQUEST) + serializer = ConnectionSerializer( + created_connection, context={'request': request}, many=False + ) + return Response(serializer.data) + + @action(methods=['post'], detail=False, serializer_class=serializers.Serializer) + def create_with_new_user(self, request): + try: + credentials = request.data['credentials'] + created_connection = create_connection_with_new_user( + credentials['create_user_via'], + credentials['user'], + credentials['password'], + request.data['nickname'], + request.data['database_name'], + request.data.get('create_database', False), + request.data.get('sample_data', []), + ) + except KeyError as e: + raise DictHasBadKeys( + message="Required key missing", + field=e.args[0] + ) + except BadInstallationTarget: + raise UnsupportedInstallationDatabase() + except IntegrityError as e: + raise IntegrityAPIException(e, status_code=status.HTTP_400_BAD_REQUEST) + serializer = ConnectionSerializer( + created_connection, context={'request': request}, many=False + ) + return Response(serializer.data) diff --git a/mathesar/api/utils.py b/mathesar/api/utils.py index 1da49dc53d..3f3a4382f6 100644 --- a/mathesar/api/utils.py +++ b/mathesar/api/utils.py @@ -1,5 +1,6 @@ from uuid import UUID from rest_framework.exceptions import NotFound +from rest_framework import status import mathesar.api.exceptions.generic_exceptions.base_exceptions as generic_api_exceptions import re @@ -8,6 +9,8 @@ from mathesar.models.base import Table from mathesar.models.query import UIQuery from mathesar.utils.preview import column_alias_from_preview_template +from mathesar.api.exceptions.generic_exceptions.base_exceptions import BadDBCredentials +import psycopg DATA_KEY = 'data' METADATA_KEY = 'metadata' @@ -158,3 +161,20 @@ def is_valid_uuid_v4(value): return True except ValueError: return False + + +def is_valid_pg_creds(credentials): + dbname = credentials["db_name"] + user = credentials["username"] + password = credentials["password"] + host = credentials["host"] + port = credentials["port"] + conn_str = f'dbname={dbname} user={user} password={password} host={host} port={port}' + try: + with psycopg.connect(conn_str): + return True + except psycopg.errors.OperationalError as e: + raise BadDBCredentials( + exception=e, + status_code=status.HTTP_400_BAD_REQUEST + ) diff --git a/mathesar/apps.py b/mathesar/apps.py index 979b67061b..f6f4db771e 100644 --- a/mathesar/apps.py +++ b/mathesar/apps.py @@ -4,13 +4,7 @@ def _prepare_database_model(**kwargs): - from mathesar.models.base import Database # noqa from mathesar.state import make_sure_initial_reflection_happened # noqa - dbs_in_settings = set(settings.DATABASES) - # We only want to track non-django dbs - dbs_in_settings.remove('default') - for db_name in dbs_in_settings: - Database.current_objects.get_or_create(name=db_name) # TODO fix test DB loading to make this unnecessary if not settings.TEST: make_sure_initial_reflection_happened() diff --git a/mathesar/data/__init__.py b/mathesar/data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mathesar/data/currency_info.json b/mathesar/data/currency_info.json new file mode 100644 index 0000000000..30eec7ba9d --- /dev/null +++ b/mathesar/data/currency_info.json @@ -0,0 +1,7933 @@ +{ + "aa_DJ.ISO8859-1": { + "int_curr_symbol": "DJF ", + "currency_symbol": "Fdj", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": "", + "grouping": [] + }, + "aa_ER": { + "int_curr_symbol": "ERN ", + "currency_symbol": "Nfk", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 0, + "frac_digits": 0, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": "", + "grouping": [] + }, + "aa_ET": { + "int_curr_symbol": "ETB ", + "currency_symbol": "Br", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "af_ZA.ISO8859-1": { + "int_curr_symbol": "ZAR ", + "currency_symbol": "R", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "agr_PE": { + "int_curr_symbol": "PEN ", + "currency_symbol": "S/", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ak_GH": { + "int_curr_symbol": "GHS ", + "currency_symbol": "GH\u20b5", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "am_ET": { + "int_curr_symbol": "ETB ", + "currency_symbol": "Br", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_US.ISO8859-1": { + "int_curr_symbol": "USD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "an_ES.ISO8859-15": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "anp_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_AE.ISO8859-6": { + "int_curr_symbol": "AED ", + "currency_symbol": "\u062f.\u0625.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_BH.ISO8859-6": { + "int_curr_symbol": "BHD ", + "currency_symbol": "\u062f.\u0628.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_DZ.ISO8859-6": { + "int_curr_symbol": "DZD ", + "currency_symbol": "\u062f.\u062c.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_EG.ISO8859-6": { + "int_curr_symbol": "EGP ", + "currency_symbol": "\u062c.\u0645.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_IQ.ISO8859-6": { + "int_curr_symbol": "IQD ", + "currency_symbol": "\u062f.\u0639.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_JO.ISO8859-6": { + "int_curr_symbol": "JOD ", + "currency_symbol": "\u062f.\u0623.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_KW.ISO8859-6": { + "int_curr_symbol": "KWD ", + "currency_symbol": "\u062f.\u0643.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_LB.ISO8859-6": { + "int_curr_symbol": "LBP ", + "currency_symbol": "\u0644.\u0644.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_LY.ISO8859-6": { + "int_curr_symbol": "LYD ", + "currency_symbol": "\u062f.\u0644.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_MA.ISO8859-6": { + "int_curr_symbol": "MAD ", + "currency_symbol": "\u062f.\u0645.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_OM.ISO8859-6": { + "int_curr_symbol": "OMR ", + "currency_symbol": "\u0631.\u0639.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_QA.ISO8859-6": { + "int_curr_symbol": "QAR ", + "currency_symbol": "\u0631.\u0642.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_SA.ISO8859-6": { + "int_curr_symbol": "SAR ", + "currency_symbol": "\u0631.\u0633", + "mon_decimal_point": ".", + "mon_thousands_sep": "", + "mon_grouping": [], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": "", + "grouping": [] + }, + "ar_SD.ISO8859-6": { + "int_curr_symbol": "SDG ", + "currency_symbol": "\u062c.\u0633.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_SS": { + "int_curr_symbol": "SSP ", + "currency_symbol": "\u00a3", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_SY.ISO8859-6": { + "int_curr_symbol": "SYP ", + "currency_symbol": "\u0644.\u0633.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_TN.ISO8859-6": { + "int_curr_symbol": "TND ", + "currency_symbol": "\u062f.\u062a.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ar_YE.ISO8859-6": { + "int_curr_symbol": "YER ", + "currency_symbol": "\u0631.\u064a.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "as_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 2, + 0 + ] + }, + "ast_ES.ISO8859-15": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ayc_PE": { + "int_curr_symbol": "PEN ", + "currency_symbol": "S/", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "az_IR": { + "int_curr_symbol": "IRR ", + "currency_symbol": "\u0631\u06cc\u0627\u0644", + "mon_decimal_point": "\u066b", + "mon_thousands_sep": "\u066c", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 0, + "frac_digits": 0, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "be_BY": { + "int_curr_symbol": "BYR ", + "currency_symbol": "\u0440\u0443\u0431", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "be_BY.UTF-8@latin": { + "int_curr_symbol": "BYR ", + "currency_symbol": "Rub", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "bem_ZM": { + "int_curr_symbol": "ZMW ", + "currency_symbol": "K", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ber_DZ": { + "int_curr_symbol": "DZD ", + "currency_symbol": "\u062f.\u062c.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ber_MA": { + "int_curr_symbol": "MAD ", + "currency_symbol": "\u2d37.\u2d4e.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "bg_BG": { + "int_curr_symbol": "BGN ", + "currency_symbol": "\u043b\u0432.", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "", + "grouping": [ + 3, + 3, + 0 + ] + }, + "bhb_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "bho_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "bho_NP": { + "int_curr_symbol": "NPR ", + "currency_symbol": "\u0930\u0942", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "bi_VU": { + "int_curr_symbol": "VUV ", + "currency_symbol": "VT", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "bn_BD": { + "int_curr_symbol": "BDT ", + "currency_symbol": "\u09f3", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 2, + 0 + ] + }, + "bn_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 2, + 0 + ] + }, + "bo_CN": { + "int_curr_symbol": "CNY ", + "currency_symbol": "\uffe5", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "bo_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "nb_NO.ISO8859-1": { + "int_curr_symbol": "NOK ", + "currency_symbol": "kr", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "br_FR.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 0 + ] + }, + "brx_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "bs_BA.ISO8859-2": { + "int_curr_symbol": "BAM ", + "currency_symbol": "KM", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "", + "grouping": [] + }, + "byn_ER": { + "int_curr_symbol": "ERN ", + "currency_symbol": "Nfk", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 0, + "frac_digits": 0, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": "", + "grouping": [] + }, + "C": { + "int_curr_symbol": "", + "currency_symbol": "", + "mon_decimal_point": "", + "mon_thousands_sep": "", + "mon_grouping": [], + "positive_sign": "", + "negative_sign": "", + "int_frac_digits": 127, + "frac_digits": 127, + "p_cs_precedes": 127, + "p_sep_by_space": 127, + "n_cs_precedes": 127, + "n_sep_by_space": 127, + "p_sign_posn": 127, + "n_sign_posn": 127, + "decimal_point": ".", + "thousands_sep": "", + "grouping": [] + }, + "fr_CA.ISO8859-1": { + "int_curr_symbol": "CAD ", + "currency_symbol": "$", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 0, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 0 + ] + }, + "en_US": { + "int_curr_symbol": "USD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ca_ES.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ca_ES.UTF-8@valencia": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ce_RU": { + "int_curr_symbol": "RUB ", + "currency_symbol": "\u20bd", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u202f", + "grouping": [ + 3, + 3, + 0 + ] + }, + "zh_CN.eucCN": { + "int_curr_symbol": "CNY ", + "currency_symbol": "\uffe5", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "zh_TW.eucTW": { + "int_curr_symbol": "TWD ", + "currency_symbol": "NT$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "chr_US": { + "int_curr_symbol": "USD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ckb_IQ": { + "int_curr_symbol": "IQD ", + "currency_symbol": "\u062f.\u0639", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "+", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "cmn_TW": { + "int_curr_symbol": "TWD ", + "currency_symbol": "NT$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 4, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 4, + 0 + ] + }, + "crh_UA": { + "int_curr_symbol": "UAH ", + "currency_symbol": "\u20b4", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "hr_HR.ISO8859-2": { + "int_curr_symbol": "HRK ", + "currency_symbol": "kn", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "cs_CZ.ISO8859-2": { + "int_curr_symbol": "CZK ", + "currency_symbol": "K\u010d", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "csb_PL": { + "int_curr_symbol": "PLN ", + "currency_symbol": "z\u0142", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u202f", + "grouping": [ + 3, + 0 + ] + }, + "cv_RU": { + "int_curr_symbol": "RUB ", + "currency_symbol": "\u20bd", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u202f", + "grouping": [ + 3, + 3, + 0 + ] + }, + "da_DK.ISO8859-1": { + "int_curr_symbol": "DKK ", + "currency_symbol": "kr.", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 2, + "n_cs_precedes": 1, + "n_sep_by_space": 2, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "de_DE.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "de_AT.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "de_BE.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "de_CH.ISO8859-1": { + "int_curr_symbol": "CHF ", + "currency_symbol": "CHF", + "mon_decimal_point": ".", + "mon_thousands_sep": "'", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ".", + "thousands_sep": "'", + "grouping": [ + 3, + 3, + 0 + ] + }, + "de_IT.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "de_LI": { + "int_curr_symbol": "CHF ", + "currency_symbol": "CHF", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u2019", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ".", + "thousands_sep": "\u2019", + "grouping": [ + 3, + 3, + 0 + ] + }, + "de_LU.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "doi_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "nl_NL.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 2, + "p_sign_posn": 1, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "nl_BE.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 2, + "p_sign_posn": 1, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "dv_MV": { + "int_curr_symbol": "MVR ", + "currency_symbol": "\u0783.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 2, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "dz_BT": { + "int_curr_symbol": "BTN ", + "currency_symbol": "Nu.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 3, + "frac_digits": 3, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 2, + 0 + ] + }, + "et_EE.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "el_GR.ISO8859-7": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 0, + "n_cs_precedes": 0, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [] + }, + "el_CY.ISO8859-7": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 0, + "n_cs_precedes": 0, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [] + }, + "en_AG": { + "int_curr_symbol": "XCD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_AU.ISO8859-1": { + "int_curr_symbol": "AUD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_BW.ISO8859-1": { + "int_curr_symbol": "BWP ", + "currency_symbol": "P", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_CA.ISO8859-1": { + "int_curr_symbol": "CAD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_DK.ISO8859-1": { + "int_curr_symbol": "DKK ", + "currency_symbol": "kr.", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 2, + "n_cs_precedes": 1, + "n_sep_by_space": 2, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_GB.ISO8859-1": { + "int_curr_symbol": "GBP ", + "currency_symbol": "\u00a3", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_HK.ISO8859-1": { + "int_curr_symbol": "HKD ", + "currency_symbol": "HK$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 0, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "en_IE.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_IL": { + "int_curr_symbol": "ILS ", + "currency_symbol": "\u20aa", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 2, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_NG": { + "int_curr_symbol": "NGN ", + "currency_symbol": "\u20a6", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_NZ.ISO8859-1": { + "int_curr_symbol": "NZD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_PH.ISO8859-1": { + "int_curr_symbol": "PHP ", + "currency_symbol": "PHP", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 0, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "en_SC": { + "int_curr_symbol": "SCR ", + "currency_symbol": "SR", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_SG.ISO8859-1": { + "int_curr_symbol": "SGD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 0, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "en_US.ISO8859-15": { + "int_curr_symbol": "USD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_ZA.ISO8859-1": { + "int_curr_symbol": "ZAR ", + "currency_symbol": "R", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_ZM": { + "int_curr_symbol": "ZMW ", + "currency_symbol": "K", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "en_ZW.ISO8859-1": { + "int_curr_symbol": "USD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "eo": { + "int_curr_symbol": "XDR ", + "currency_symbol": "\u00a4", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "", + "grouping": [] + }, + "eo_US": { + "int_curr_symbol": "USD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_ES.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_AR.ISO8859-1": { + "int_curr_symbol": "ARS ", + "currency_symbol": "$", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_BO.ISO8859-1": { + "int_curr_symbol": "BOB ", + "currency_symbol": "Bs", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_CL.ISO8859-1": { + "int_curr_symbol": "CLP ", + "currency_symbol": "$", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_CO.ISO8859-1": { + "int_curr_symbol": "COP ", + "currency_symbol": "$", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_CR.ISO8859-1": { + "int_curr_symbol": "CRC ", + "currency_symbol": "C=", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_CU": { + "int_curr_symbol": "CUP ", + "currency_symbol": "$", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [] + }, + "es_DO.ISO8859-1": { + "int_curr_symbol": "DOP ", + "currency_symbol": "RD$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_EC.ISO8859-1": { + "int_curr_symbol": "USD ", + "currency_symbol": "$", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_GT.ISO8859-1": { + "int_curr_symbol": "GTQ ", + "currency_symbol": "Q", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_HN.ISO8859-1": { + "int_curr_symbol": "HNL ", + "currency_symbol": "L", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_MX.ISO8859-1": { + "int_curr_symbol": "MXN ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": " ", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_NI.ISO8859-1": { + "int_curr_symbol": "NIO ", + "currency_symbol": "C$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_PA.ISO8859-1": { + "int_curr_symbol": "PAB ", + "currency_symbol": "B/.", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_PE.ISO8859-1": { + "int_curr_symbol": "PEN ", + "currency_symbol": "S/", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_PR.ISO8859-1": { + "int_curr_symbol": "USD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_PY.ISO8859-1": { + "int_curr_symbol": "PYG ", + "currency_symbol": "Gs.", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_SV.ISO8859-1": { + "int_curr_symbol": "USD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_US.ISO8859-1": { + "int_curr_symbol": "USD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_UY.ISO8859-1": { + "int_curr_symbol": "UYU ", + "currency_symbol": "$", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "es_VE.ISO8859-1": { + "int_curr_symbol": "VEF ", + "currency_symbol": "Bs.", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "et_EE.ISO8859-15": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "eu_ES.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "eu_FR.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 0 + ] + }, + "fa_IR": { + "int_curr_symbol": "IRR ", + "currency_symbol": "\u0631\u06cc\u0627\u0644", + "mon_decimal_point": "\u066b", + "mon_thousands_sep": "\u066c", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 0, + "frac_digits": 0, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ff_SN": { + "int_curr_symbol": "XOF ", + "currency_symbol": "CFA", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 0 + ] + }, + "fil_PH": { + "int_curr_symbol": "PHP ", + "currency_symbol": "\u20b1", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "fi_FI.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "fo_FO.ISO8859-1": { + "int_curr_symbol": "DKK ", + "currency_symbol": "kr.", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 2, + "n_cs_precedes": 1, + "n_sep_by_space": 2, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "fr_FR.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 0 + ] + }, + "fr_BE.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "fr_CH.ISO8859-1": { + "int_curr_symbol": "CHF ", + "currency_symbol": "CHF", + "mon_decimal_point": ".", + "mon_thousands_sep": "'", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ".", + "thousands_sep": "'", + "grouping": [ + 3, + 3, + 0 + ] + }, + "fr_LU.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "fur_IT": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "fy_DE": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "fy_NL": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 2, + "p_sign_posn": 1, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ga_IE.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "gl_ES.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "", + "grouping": [] + }, + "gez_ER": { + "int_curr_symbol": "ERN ", + "currency_symbol": "Nfk", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 0, + "frac_digits": 0, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": "", + "grouping": [] + }, + "gez_ET": { + "int_curr_symbol": "ETB ", + "currency_symbol": "Br", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "gu_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "gv_GB.ISO8859-1": { + "int_curr_symbol": "GBP ", + "currency_symbol": "\u00a3", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ha_NG": { + "int_curr_symbol": "NGN ", + "currency_symbol": "\u20a6", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "hak_TW": { + "int_curr_symbol": "TWD ", + "currency_symbol": "NT$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 4, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 4, + 0 + ] + }, + "he_IL.ISO8859-8": { + "int_curr_symbol": "ILS ", + "currency_symbol": "ILS", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 2, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "hif_FJ": { + "int_curr_symbol": "FJD ", + "currency_symbol": "FJ$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "hne_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "hsb_DE.ISO8859-2": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ht_HT": { + "int_curr_symbol": "HTG ", + "currency_symbol": "g", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u202f", + "grouping": [ + 3, + 0 + ] + }, + "hu_HU.ISO8859-2": { + "int_curr_symbol": "HUF ", + "currency_symbol": "Ft", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "hy_AM": { + "int_curr_symbol": "AMD ", + "currency_symbol": "\u058f", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ia_FR": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u202f", + "grouping": [ + 3, + 0 + ] + }, + "is_IS.ISO8859-1": { + "int_curr_symbol": "ISK ", + "currency_symbol": "kr", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 0, + "frac_digits": 0, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "id_ID.ISO8859-1": { + "int_curr_symbol": "IDR ", + "currency_symbol": "Rp", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ig_NG": { + "int_curr_symbol": "NGN ", + "currency_symbol": "\u20a6", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ik_CA": { + "int_curr_symbol": "CAD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "it_IT.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "it_CH.ISO8859-1": { + "int_curr_symbol": "CHF ", + "currency_symbol": "CHF", + "mon_decimal_point": ".", + "mon_thousands_sep": "'", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ".", + "thousands_sep": "'", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ja_JP.eucJP": { + "int_curr_symbol": "JPY ", + "currency_symbol": "\uffe5", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 0, + "frac_digits": 0, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ka_GE.GEORGIAN-PS": { + "int_curr_symbol": "GEL ", + "currency_symbol": "GEL", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "kab_DZ": { + "int_curr_symbol": "DZD ", + "currency_symbol": "DA", + "mon_decimal_point": ",", + "mon_thousands_sep": "", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "", + "grouping": [ + 3, + 0 + ] + }, + "kl_GL.ISO8859-1": { + "int_curr_symbol": "DKK ", + "currency_symbol": "kr.", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 2, + "n_cs_precedes": 1, + "n_sep_by_space": 2, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "km_KH": { + "int_curr_symbol": "KHR ", + "currency_symbol": "\u17db", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 0, + "n_cs_precedes": 0, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "kn_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ko_KR.eucKR": { + "int_curr_symbol": "KRW ", + "currency_symbol": "KRW", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 0, + "frac_digits": 0, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 4, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "kok_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ks_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ks_IN.UTF-8@devanagari": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ku_TR.ISO8859-9": { + "int_curr_symbol": "TRY ", + "currency_symbol": "TL", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "kw_GB.ISO8859-1": { + "int_curr_symbol": "GBP ", + "currency_symbol": "\u00a3", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ky_KG": { + "int_curr_symbol": "KGS ", + "currency_symbol": "\u0441\u043e\u043c", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u202f", + "grouping": [ + 3, + 3, + 0 + ] + }, + "lb_LU": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "lg_UG.ISO8859-10": { + "int_curr_symbol": "UGX ", + "currency_symbol": "USh", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 0, + "n_cs_precedes": 0, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "li_BE": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 2, + "p_sign_posn": 1, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "li_NL": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 2, + "p_sign_posn": 1, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "lij_IT": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "lt_LT.ISO8859-13": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ln_CD": { + "int_curr_symbol": "CDF ", + "currency_symbol": "FC", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "", + "grouping": [ + 3, + 0 + ] + }, + "lv_LV.ISO8859-13": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "lzh_TW": { + "int_curr_symbol": "TWD ", + "currency_symbol": "NT$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 4, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 4, + 0 + ] + }, + "mag_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "mai_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "mai_NP": { + "int_curr_symbol": "NPR ", + "currency_symbol": "\u0930\u0942", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "mfe_MU": { + "int_curr_symbol": "MUR ", + "currency_symbol": "\u20a8", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": "\u202f", + "grouping": [ + 3, + 0 + ] + }, + "mg_MG.ISO8859-15": { + "int_curr_symbol": "MGA ", + "currency_symbol": "Ar", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "", + "grouping": [] + }, + "mhr_RU": { + "int_curr_symbol": "RUB ", + "currency_symbol": "\u20bd", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u202f", + "grouping": [ + 3, + 3, + 0 + ] + }, + "miq_NI": { + "int_curr_symbol": "NIO ", + "currency_symbol": "C$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "mjw_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 2, + 0 + ] + }, + "mk_MK.ISO8859-5": { + "int_curr_symbol": "MKD ", + "currency_symbol": "\u0434\u0435\u043d", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ml_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 2, + 0 + ] + }, + "mn_MN": { + "int_curr_symbol": "MNT ", + "currency_symbol": "\u20ae", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "mni_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 2, + 0 + ] + }, + "mr_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ms_MY.ISO8859-1": { + "int_curr_symbol": "MYR ", + "currency_symbol": "RM", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 0, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "mt_MT.ISO8859-3": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "my_MM": { + "int_curr_symbol": "MMK ", + "currency_symbol": "K", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 0, + "n_cs_precedes": 0, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "nan_TW": { + "int_curr_symbol": "TWD ", + "currency_symbol": "NT$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 4, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 4, + 0 + ] + }, + "nds_DE": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "nds_NL": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 2, + "p_sign_posn": 1, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ne_NP": { + "int_curr_symbol": "NPR ", + "currency_symbol": "\u0930\u0942", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "nhn_MX": { + "int_curr_symbol": "MXN ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": " ", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": "\u202f", + "grouping": [ + 3, + 3, + 0 + ] + }, + "niu_NU": { + "int_curr_symbol": "NZD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "niu_NZ": { + "int_curr_symbol": "NZD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "nl_AW": { + "int_curr_symbol": "AWG ", + "currency_symbol": "Afl.", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 2, + "p_sign_posn": 1, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "nn_NO.ISO8859-1": { + "int_curr_symbol": "NOK ", + "currency_symbol": "kr", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 3, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "oc_FR.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 0 + ] + }, + "om_ET": { + "int_curr_symbol": "ETB ", + "currency_symbol": "Br", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "om_KE.ISO8859-1": { + "int_curr_symbol": "KES ", + "currency_symbol": "Ksh", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "or_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 2, + 0 + ] + }, + "os_RU": { + "int_curr_symbol": "RUB ", + "currency_symbol": "\u20bd", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u202f", + "grouping": [ + 3, + 3, + 0 + ] + }, + "pa_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "pa_PK": { + "int_curr_symbol": "PKR ", + "currency_symbol": "Rs", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 2, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "pap_AW": { + "int_curr_symbol": "AWG ", + "currency_symbol": "\u0192", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ",", + "thousands_sep": "", + "grouping": [] + }, + "pap_CW": { + "int_curr_symbol": "ANG ", + "currency_symbol": "\u0192", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 2, + "decimal_point": ",", + "thousands_sep": "", + "grouping": [] + }, + "pl_PL.ISO8859-2": { + "int_curr_symbol": "PLN ", + "currency_symbol": "z\u0142", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 0 + ] + }, + "pt_PT.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "", + "grouping": [] + }, + "pt_BR.ISO8859-1": { + "int_curr_symbol": "BRL ", + "currency_symbol": "R$", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ps_AF": { + "int_curr_symbol": "AFN ", + "currency_symbol": "\u060b", + "mon_decimal_point": "\u066b", + "mon_thousands_sep": "\u066c", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 0, + "frac_digits": 0, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": "\u066b", + "thousands_sep": "\u066c", + "grouping": [ + 3, + 0 + ] + }, + "quz_PE": { + "int_curr_symbol": "PEN ", + "currency_symbol": "S/", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "raj_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ro_RO.ISO8859-2": { + "int_curr_symbol": "RON ", + "currency_symbol": "Lei", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ru_RU": { + "int_curr_symbol": "RUB ", + "currency_symbol": "\u20bd", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u202f", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ru_UA.KOI8-U": { + "int_curr_symbol": "UAH ", + "currency_symbol": "\u0433\u0440\u043d", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ru_RU.KOI8-R": { + "int_curr_symbol": "RUB ", + "currency_symbol": "\u0440\u0443\u0431", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "sa_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "sat_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "sc_IT": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "sd_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "sd_IN.UTF-8@devanagari": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "sd_PK": { + "int_curr_symbol": "PKR ", + "currency_symbol": "\u20a8", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 2, + 0 + ] + }, + "se_NO": { + "int_curr_symbol": "NOK ", + "currency_symbol": "kr", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "sr_RS.UTF-8@latin": { + "int_curr_symbol": "RSD ", + "currency_symbol": "din", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 0, + "frac_digits": 0, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "", + "grouping": [] + }, + "sgs_LT": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "shn_MM": { + "int_curr_symbol": "MMK ", + "currency_symbol": "Ks", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 0, + "n_cs_precedes": 0, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "shs_CA": { + "int_curr_symbol": "CAD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "si_LK": { + "int_curr_symbol": "LKR ", + "currency_symbol": "\u0dbb\u0dd4", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "sid_ET": { + "int_curr_symbol": "ETB ", + "currency_symbol": "Br", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "sk_SK.ISO8859-2": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "sl_SI.ISO8859-2": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [] + }, + "sm_WS": { + "int_curr_symbol": "WST ", + "currency_symbol": "WS$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "so_DJ.ISO8859-1": { + "int_curr_symbol": "DJF ", + "currency_symbol": "Fdj", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": "", + "grouping": [] + }, + "so_ET": { + "int_curr_symbol": "ETB ", + "currency_symbol": "Br", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "so_KE.ISO8859-1": { + "int_curr_symbol": "KES ", + "currency_symbol": "Ksh", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "so_SO.ISO8859-1": { + "int_curr_symbol": "SOS ", + "currency_symbol": "S", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "sq_MK": { + "int_curr_symbol": "MKD ", + "currency_symbol": "den", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 0 + ] + }, + "sr_RS": { + "int_curr_symbol": "RSD ", + "currency_symbol": "\u0434\u0438\u043d", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "", + "grouping": [] + }, + "sr_ME": { + "int_curr_symbol": "EUR ", + "currency_symbol": "\u20ac", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "", + "grouping": [] + }, + "st_ZA.ISO8859-1": { + "int_curr_symbol": "ZAR ", + "currency_symbol": "R", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "sv_SE.ISO8859-1": { + "int_curr_symbol": "SEK ", + "currency_symbol": "kr", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "sv_FI.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "sw_KE": { + "int_curr_symbol": "KES ", + "currency_symbol": "Ksh", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "sw_TZ": { + "int_curr_symbol": "TZS ", + "currency_symbol": "TSh", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "szl_PL": { + "int_curr_symbol": "PLN ", + "currency_symbol": "z\u0142", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u202f", + "grouping": [ + 3, + 0 + ] + }, + "ta_LK": { + "int_curr_symbol": "LKR ", + "currency_symbol": "\u0dbb\u0dd4", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 2, + 0 + ] + }, + "tcy_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "te_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 2, + 0 + ] + }, + "th_TH": { + "int_curr_symbol": "THB ", + "currency_symbol": "\u0e3f", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 2, + "n_cs_precedes": 1, + "n_sep_by_space": 2, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "the_NP": { + "int_curr_symbol": "NPR ", + "currency_symbol": "\u0930\u0942", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "ti_ER": { + "int_curr_symbol": "ERN ", + "currency_symbol": "Nfk", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 0, + "frac_digits": 0, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": "", + "grouping": [] + }, + "ti_ET": { + "int_curr_symbol": "ETB ", + "currency_symbol": "Br", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "tig_ER": { + "int_curr_symbol": "ERN ", + "currency_symbol": "Nfk", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 0, + "frac_digits": 0, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": "", + "grouping": [] + }, + "tk_TM": { + "int_curr_symbol": "TMM ", + "currency_symbol": "MANAT", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "tl_PH.ISO8859-1": { + "int_curr_symbol": "PHP ", + "currency_symbol": "PHP", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "to_TO": { + "int_curr_symbol": "TOP ", + "currency_symbol": "T$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "tpi_PG": { + "int_curr_symbol": "PGK ", + "currency_symbol": "K", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "tr_TR.ISO8859-9": { + "int_curr_symbol": "TRY ", + "currency_symbol": "TL", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "tr_CY.ISO8859-9": { + "int_curr_symbol": "TRY ", + "currency_symbol": "TL", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "tt_RU.UTF-8@iqtelif": { + "int_curr_symbol": "RUB ", + "currency_symbol": "\u20bd", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ug_CN": { + "int_curr_symbol": "CNY ", + "currency_symbol": "\uffe5", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "uk_UA.KOI8-U": { + "int_curr_symbol": "UAH ", + "currency_symbol": "\u0433\u0440\u043d.", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u00a0", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 2, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "\u00a0", + "grouping": [ + 3, + 3, + 0 + ] + }, + "unm_US": { + "int_curr_symbol": "USD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": "\u202f", + "grouping": [ + 2, + 2, + 2, + 3, + 0 + ] + }, + "ur_IN": { + "int_curr_symbol": "INR ", + "currency_symbol": "\u20b9", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 2, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "uz_UZ": { + "int_curr_symbol": "UZS ", + "currency_symbol": "so\u02bbm", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "ve_ZA": { + "int_curr_symbol": "ZAR ", + "currency_symbol": "R", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "wa_BE.ISO8859-1": { + "int_curr_symbol": "EUR ", + "currency_symbol": "EUR", + "mon_decimal_point": ",", + "mon_thousands_sep": ".", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": ".", + "grouping": [ + 3, + 3, + 0 + ] + }, + "wae_CH": { + "int_curr_symbol": "CHF ", + "currency_symbol": "CHF", + "mon_decimal_point": ".", + "mon_thousands_sep": "\u2019", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ".", + "thousands_sep": "\u2019", + "grouping": [ + 3, + 3, + 0 + ] + }, + "wal_ET": { + "int_curr_symbol": "ETB ", + "currency_symbol": "Br", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "wo_SN": { + "int_curr_symbol": "XOF ", + "currency_symbol": "CFA", + "mon_decimal_point": ",", + "mon_thousands_sep": "\u202f", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 0, + "p_sep_by_space": 1, + "n_cs_precedes": 0, + "n_sep_by_space": 1, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ",", + "thousands_sep": "", + "grouping": [] + }, + "xh_ZA.ISO8859-1": { + "int_curr_symbol": "ZAR ", + "currency_symbol": "R", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "yi_US": { + "int_curr_symbol": "USD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 1, + "n_cs_precedes": 1, + "n_sep_by_space": 1, + "p_sign_posn": 2, + "n_sign_posn": 2, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "yo_NG": { + "int_curr_symbol": "NGN ", + "currency_symbol": "\u20a6", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "yue_HK": { + "int_curr_symbol": "HKD ", + "currency_symbol": "HK$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 0, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "yuw_PG": { + "int_curr_symbol": "PGK ", + "currency_symbol": "K", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + }, + "zh_CN": { + "int_curr_symbol": "CNY ", + "currency_symbol": "\uffe5", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 4, + "n_sign_posn": 4, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "zh_TW": { + "int_curr_symbol": "TWD ", + "currency_symbol": "NT$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "zh_HK.big5hkscs": { + "int_curr_symbol": "HKD ", + "currency_symbol": "HK$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 0, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "zh_SG": { + "int_curr_symbol": "SGD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 0, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "zh_SG.GBK": { + "int_curr_symbol": "SGD ", + "currency_symbol": "$", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 0, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 0 + ] + }, + "zu_ZA.ISO8859-1": { + "int_curr_symbol": "ZAR ", + "currency_symbol": "R", + "mon_decimal_point": ".", + "mon_thousands_sep": ",", + "mon_grouping": [ + 3, + 3, + 0 + ], + "positive_sign": "", + "negative_sign": "-", + "int_frac_digits": 2, + "frac_digits": 2, + "p_cs_precedes": 1, + "p_sep_by_space": 0, + "n_cs_precedes": 1, + "n_sep_by_space": 0, + "p_sign_posn": 1, + "n_sign_posn": 1, + "decimal_point": ".", + "thousands_sep": ",", + "grouping": [ + 3, + 3, + 0 + ] + } +} \ No newline at end of file diff --git a/mathesar/database/base.py b/mathesar/database/base.py index a1d7cdbbe6..d6384d3d76 100644 --- a/mathesar/database/base.py +++ b/mathesar/database/base.py @@ -1,44 +1,20 @@ -from django.conf import settings - -from demo.utils import get_is_live_demo_mode - from db import engine -DEFAULT_DB = 'default' - -def create_mathesar_engine(db_name): +def create_mathesar_engine(db_model): """Create an SQLAlchemy engine using stored credentials.""" import logging logger = logging.getLogger('create_mathesar_engine') logger.debug('enter') - try: - credentials = _get_credentials_for_db_name_in_settings(db_name) - except KeyError: - if get_is_live_demo_mode(): - credentials = _get_credentials_for_db_name_not_in_settings(db_name) - else: - raise + credentials = _get_credentials_for_db_model(db_model) return engine.create_future_engine_with_custom_types(**credentials) -def _get_credentials_for_db_name_in_settings(db_name): - settings_entry = settings.DATABASES[db_name] - return dict( - username=settings_entry["USER"], - password=settings_entry["PASSWORD"], - hostname=settings_entry["HOST"], - database=settings_entry["NAME"], - port=settings_entry["PORT"], - ) - - -def _get_credentials_for_db_name_not_in_settings(db_name): - settings_entry = settings.DATABASES[DEFAULT_DB] +def _get_credentials_for_db_model(db_model): return dict( - username=settings_entry["USER"], - password=settings_entry["PASSWORD"], - hostname=settings_entry["HOST"], - database=db_name, - port=settings_entry["PORT"], + username=db_model.username, + password=db_model.password, + hostname=db_model.host, + database=db_model.db_name, + port=db_model.port, ) diff --git a/mathesar/database/types.py b/mathesar/database/types.py index 414ca8a772..46c827640f 100644 --- a/mathesar/database/types.py +++ b/mathesar/database/types.py @@ -3,17 +3,13 @@ of db.types.base.DatabaseType). """ from enum import Enum -from collections.abc import Collection from db.types.base import ( - DatabaseType, PostgresType, MathesarCustomType + PostgresType, MathesarCustomType ) from db.types.hintsets import db_types_hinted class UIType(Enum): - id: str # noqa: NT001 - display_name: str # noqa: NT001 - db_types: Collection[DatabaseType] # noqa: NT001 BOOLEAN = ( 'boolean', diff --git a/mathesar/exception_handlers.py b/mathesar/exception_handlers.py index 5ca9cef79d..6f491edddf 100644 --- a/mathesar/exception_handlers.py +++ b/mathesar/exception_handlers.py @@ -6,8 +6,8 @@ from django.utils.encoding import force_str from rest_framework.views import exception_handler from rest_framework_friendly_errors.settings import FRIENDLY_EXCEPTION_DICT -from sqlalchemy.exc import IntegrityError, ProgrammingError - +from sqlalchemy.exc import IntegrityError, ProgrammingError, OperationalError as sqla_OperationalError +from psycopg.errors import OperationalError as pspg_OperationalError from db.types.exceptions import UnsupportedTypeException from mathesar.api.exceptions.database_exceptions import ( base_exceptions as base_api_exceptions, @@ -26,11 +26,15 @@ ProgrammingError: lambda exc: base_api_exceptions.ProgrammingAPIException(exc), URLDownloadError: lambda exc: data_import_api_exceptions.URLDownloadErrorAPIException(exc), URLNotReachable: lambda exc: data_import_api_exceptions.URLNotReachableAPIException(exc), - URLInvalidContentTypeError: lambda exc: data_import_api_exceptions.URLInvalidContentTypeAPIException(exc) + URLInvalidContentTypeError: lambda exc: data_import_api_exceptions.URLInvalidContentTypeAPIException(exc), + sqla_OperationalError: lambda exc: base_api_exceptions.InvalidDBConnection(exc), + pspg_OperationalError: lambda exc: base_api_exceptions.InvalidDBConnection(exc) } def standardize_error_response(data): + if isinstance(data, dict): + data = [data] for index, error in enumerate(data): if 'code' in error: if error['code'] is not None and str(error['code']) != 'None': @@ -92,6 +96,8 @@ def mathesar_exception_handler(exc, context): def is_pretty(data): + if isinstance(data, dict): + data = [data] if not isinstance(data, list): return False else: diff --git a/mathesar/filters/__init__.py b/mathesar/filters/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mathesar/imports/csv.py b/mathesar/imports/csv.py index 1f70f67d89..e1b45236c5 100644 --- a/mathesar/imports/csv.py +++ b/mathesar/imports/csv.py @@ -143,8 +143,8 @@ def insert_records_from_csv_data_file(name, schema, column_names, engine, commen def create_db_table_from_csv_data_file(data_file, name, schema, comment=None): - db_name = schema.database.name - engine = create_mathesar_engine(db_name) + db_model = schema.database + engine = create_mathesar_engine(db_model) sv_filename = data_file.file.path header = data_file.header dialect = csv.dialect.SimpleDialect(data_file.delimiter, data_file.quotechar, @@ -158,6 +158,6 @@ def create_db_table_from_csv_data_file(data_file, name, schema, comment=None): except (IntegrityError, DataError): drop_table(name=name, schema=schema.name, engine=engine) column_names_alt = get_alternate_column_names(column_names) - insert_records_from_csv_data_file(name, schema, column_names_alt, engine, comment, data_file) - reset_reflection(db_name=db_name) + table = insert_records_from_csv_data_file(name, schema, column_names_alt, engine, comment, data_file) + reset_reflection(db_name=db_model.name) return table diff --git a/mathesar/imports/excel.py b/mathesar/imports/excel.py index 9775181d2d..74d1c36b08 100644 --- a/mathesar/imports/excel.py +++ b/mathesar/imports/excel.py @@ -1,5 +1,6 @@ import pandas +from db.constants import ID, ID_ORIGINAL from db.tables.operations.alter import update_pk_sequence_to_latest from mathesar.database.base import create_mathesar_engine from db.records.operations.insert import insert_records_from_excel @@ -7,6 +8,7 @@ from db.tables.operations.drop import drop_table from mathesar.imports.utils import get_alternate_column_names, process_column_names from psycopg2.errors import IntegrityError, DataError +from sqlalchemy.exc import IntegrityError as sqlalchemy_integrity_error from mathesar.state import reset_reflection @@ -19,7 +21,8 @@ def insert_records_from_dataframe(name, schema, column_names, engine, comment, d engine=engine, comment=comment, ) - + if ID_ORIGINAL in column_names: + dataframe.rename(columns={ID: ID_ORIGINAL}, inplace=True) insert_records_from_excel( table, engine, @@ -28,18 +31,37 @@ def insert_records_from_dataframe(name, schema, column_names, engine, comment, d return table +def remove_empty_rows_and_columns_from_dataframe(df): + if df.iloc[0].isna().any(): + + # drop rows with all NaN values + df.dropna(how='all', inplace=True) + + # drop columns with all NaN values + df.dropna(axis=1, how='all', inplace=True) + + if all(df.columns.str.startswith('Unnamed')): + df.columns = df.iloc[0] + df = df[1:] + + return df + + def create_db_table_from_excel_data_file(data_file, name, schema, comment=None): - db_name = schema.database.name - engine = create_mathesar_engine(db_name) - dataframe = pandas.read_excel(data_file.file.path) + db_model = schema.database + engine = create_mathesar_engine(db_model) + header_row = 0 if data_file.header else None + dataframe = remove_empty_rows_and_columns_from_dataframe( + pandas.read_excel(data_file.file.path, data_file.sheet_index, header=header_row) + ) column_names = process_column_names(dataframe.columns) try: table = insert_records_from_dataframe(name, schema, column_names, engine, comment, dataframe) update_pk_sequence_to_latest(engine, table) - except (IntegrityError, DataError): + except (IntegrityError, DataError, sqlalchemy_integrity_error): drop_table(name=name, schema=schema.name, engine=engine) column_names_alt = get_alternate_column_names(column_names) table = insert_records_from_dataframe(name, schema, column_names_alt, engine, comment, dataframe) - reset_reflection(db_name=db_name) + reset_reflection(db_name=db_model.name) return table diff --git a/mathesar/imports/json.py b/mathesar/imports/json.py index 57c0f2bfe5..7cfe93097d 100644 --- a/mathesar/imports/json.py +++ b/mathesar/imports/json.py @@ -11,6 +11,7 @@ ) from mathesar.imports.utils import get_alternate_column_names, process_column_names from psycopg2.errors import IntegrityError, DataError +from sqlalchemy.exc import IntegrityError as sqlalchemy_integrity_error from mathesar.state import reset_reflection @@ -82,8 +83,8 @@ def insert_records_from_json_data_file(name, schema, column_names, engine, comme def create_db_table_from_json_data_file(data_file, name, schema, comment=None): - db_name = schema.database.name - engine = create_mathesar_engine(db_name) + db_model = schema.database + engine = create_mathesar_engine(db_model) json_filepath = data_file.file.path max_level = data_file.max_level column_names = process_column_names( @@ -92,10 +93,10 @@ def create_db_table_from_json_data_file(data_file, name, schema, comment=None): try: table = insert_records_from_json_data_file(name, schema, column_names, engine, comment, json_filepath, max_level) update_pk_sequence_to_latest(engine, table) - except (IntegrityError, DataError): + except (IntegrityError, DataError, sqlalchemy_integrity_error): drop_table(name=name, schema=schema.name, engine=engine) column_names_alt = get_alternate_column_names(column_names) table = insert_records_from_json_data_file(name, schema, column_names_alt, engine, comment, json_filepath, max_level) - reset_reflection(db_name=db_name) + reset_reflection(db_name=db_model.name) return table diff --git a/install.py b/mathesar/install.py similarity index 61% rename from install.py rename to mathesar/install.py index cddc47f7bf..3efaa9bd00 100644 --- a/install.py +++ b/mathesar/install.py @@ -9,10 +9,12 @@ from django.core import management from decouple import config as decouple_config from django.conf import settings +from django.db.utils import IntegrityError +from sqlalchemy.exc import OperationalError from db import install -def main(): +def main(skip_static_collection=False): # skip_confirm is temporarily enabled by default as we don't have any use # for interactive prompts with docker only deployments skip_confirm = True @@ -25,24 +27,34 @@ def main(): management.call_command('migrate') debug_mode = decouple_config('DEBUG', default=False, cast=bool) # - if not debug_mode: + if not debug_mode and not skip_static_collection: management.call_command('collectstatic', '--noinput', '--clear') print("------------Setting up User Databases------------") django_db_key = decouple_config('DJANGO_DATABASE_KEY', default="default") user_databases = [key for key in settings.DATABASES if key != django_db_key] for database_key in user_databases: - install_on_db_with_key(database_key, skip_confirm) + try: + install_on_db_with_key(database_key, skip_confirm) + except IntegrityError: + continue def install_on_db_with_key(database_key, skip_confirm): - install.install_mathesar( - database_name=settings.DATABASES[database_key]["NAME"], - username=settings.DATABASES[database_key]["USER"], - password=settings.DATABASES[database_key]["PASSWORD"], - hostname=settings.DATABASES[database_key]["HOST"], - port=settings.DATABASES[database_key]["PORT"], - skip_confirm=skip_confirm - ) + from mathesar.models.base import Database + db_model = Database.create_from_settings_key(database_key) + db_model.save() + try: + install.install_mathesar( + database_name=db_model.db_name, + hostname=db_model.host, + username=db_model.username, + password=db_model.password, + port=db_model.port, + skip_confirm=skip_confirm + ) + except OperationalError as e: + db_model.delete() + raise e if __name__ == "__main__": diff --git a/mathesar/migrations/0005_release_0_1_4.py b/mathesar/migrations/0005_release_0_1_4.py new file mode 100644 index 0000000000..04df9ca932 --- /dev/null +++ b/mathesar/migrations/0005_release_0_1_4.py @@ -0,0 +1,94 @@ +from django.db import migrations, models, connection +import django.contrib.postgres.fields +import encrypted_fields.fields +import mathesar.models.base + + +def column_order_to_jsonb_postgres_fwd(apps, schema_editor): + if connection.settings_dict['ENGINE'].startswith('django.db.backends.postgresql'): + schema_editor.execute('ALTER TABLE mathesar_tablesettings ALTER column_order TYPE jsonb USING array_to_json(column_order)') + + # Adds validators, converts type on SQLite + migrations.AlterField( + model_name='tablesettings', + name='column_order', + field=models.JSONField(blank=True, default=None, null=True, validators=[mathesar.models.base.validate_column_order]), + ), + + +def column_order_to_jsonb_postgres_rev(apps, schema_editor): + if connection.settings_dict['ENGINE'].startswith('django.db.backends.postgresql'): + schema_editor.execute('ALTER TABLE mathesar_tablesettings ALTER column_order TYPE integer[] USING translate(column_order::text, \'[]\', \'{}\')::integer[]') + else: + # Reverts to the initial state as mentioned in 0001_initial.py for the sake of consistency + migrations.AlterField( + model_name='tablesettings', + name='column_order', + field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), default=None, null=True, size=None), + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('mathesar', '0004_shares'), + ] + + operations = [ + migrations.AddField( + model_name='database', + name='db_name', + field=models.CharField(default='', max_length=128), + preserve_default=False, + ), + migrations.AddField( + model_name='database', + name='host', + field=models.CharField(default='', max_length=255), + preserve_default=False, + ), + migrations.AddField( + model_name='database', + name='password', + field=encrypted_fields.fields.EncryptedCharField(default='mathesar', max_length=255), + preserve_default=False, + ), + migrations.AddField( + model_name='database', + name='port', + field=models.IntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='database', + name='username', + field=encrypted_fields.fields.EncryptedCharField(default='mathesar', max_length=255), + preserve_default=False, + ), + migrations.AddField( + model_name='datafile', + name='sheet_index', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='user', + name='display_language', + field=models.CharField(blank=True, default='en', max_length=30), + ), + migrations.AlterField( + model_name='constraint', + name='oid', + field=models.PositiveIntegerField(), + ), + migrations.AlterField( + model_name='schema', + name='oid', + field=models.PositiveIntegerField(), + ), + migrations.AlterField( + model_name='table', + name='oid', + field=models.PositiveIntegerField(), + ), + migrations.RunPython(column_order_to_jsonb_postgres_fwd, column_order_to_jsonb_postgres_rev), + ] diff --git a/mathesar/migrations/0006_mathesar_databases_to_model.py b/mathesar/migrations/0006_mathesar_databases_to_model.py new file mode 100644 index 0000000000..741796cb72 --- /dev/null +++ b/mathesar/migrations/0006_mathesar_databases_to_model.py @@ -0,0 +1,34 @@ +from decouple import config as decouple_config +from django.conf import settings +from django.db import migrations + + +def update_conn_info(apps, _): + """Add info from MATHESAR_DATABASES to new model fields.""" + Database = apps.get_model('mathesar', 'Database') + django_db_key = decouple_config('DJANGO_DATABASE_KEY', default="default") + user_databases = [key for key in settings.DATABASES if key != django_db_key] + for database_key in user_databases: + try: + db = Database.current_objects.get(name=database_key) + except Database.DoesNotExist: + continue + db_info = settings.DATABASES[database_key] + if 'postgres' in db_info['ENGINE']: + db.name = database_key + db.db_name = db_info['NAME'] + db.username = db_info['USER'] + db.password = db_info['PASSWORD'] + db.host = db_info['HOST'] + db.port = db_info['PORT'] + db.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('mathesar', '0005_release_0_1_4'), + ] + operations = [ + migrations.RunPython(update_conn_info), + ] diff --git a/mathesar/models/base.py b/mathesar/models/base.py index 17a813fb8c..60b2b5059b 100644 --- a/mathesar/models/base.py +++ b/mathesar/models/base.py @@ -7,13 +7,13 @@ from django.core.exceptions import ValidationError from django.db import models from django.db.models import JSONField -from django.contrib.postgres.fields import ArrayField - +from encrypted_fields.fields import EncryptedCharField from db.columns import utils as column_utils from db.columns.operations.create import create_column, duplicate_column from db.columns.operations.alter import alter_column from db.columns.operations.drop import drop_column from db.columns.operations.select import ( + get_column_description, get_column_attnum_from_names_as_map, get_column_name_from_attnum, get_map_of_attnum_to_column_name, get_map_of_attnum_and_table_oid_to_column_name, ) @@ -92,7 +92,7 @@ class DatabaseObject(ReflectionManagerMixin, BaseModel): """ Objects that can be referenced using a database identifier """ - oid = models.IntegerField() + oid = models.PositiveIntegerField() class Meta: abstract = True @@ -110,10 +110,15 @@ def __repr__(self): class Database(ReflectionManagerMixin, BaseModel): + name = models.CharField(max_length=128, unique=True) + db_name = models.CharField(max_length=128) + username = EncryptedCharField(max_length=255) + password = EncryptedCharField(max_length=255) + host = models.CharField(max_length=255) + port = models.IntegerField() current_objects = models.Manager() # TODO does this need to be defined, given that ReflectionManagerMixin defines an identical attribute? objects = DatabaseObjectManager() - name = models.CharField(max_length=128, unique=True) deleted = models.BooleanField(blank=True, default=False) @property @@ -126,7 +131,7 @@ def _sa_engine(self): engine = _engine_cache.get(db_name) model_utils.ensure_cached_engine_ready(engine) else: - engine = create_mathesar_engine(db_name=db_name) + engine = create_mathesar_engine(self) _engine_cache[db_name] = engine return engine @@ -141,6 +146,36 @@ def supported_ui_types(self): def __repr__(self): return f'{self.__class__.__name__}: {self.name}, {self.id}' + @classmethod + def create_from_settings_key(cls, db_key): + """ + Get an ethereal instance of the model from Django settings. + + This is only supported for Postgres DBs (e.g., it won't work on an + SQLite3 internal DB; that returns NoneType) + + Args: + db_key: This should be the key of the DB in settings.DATABASES + """ + db_info = settings.DATABASES[db_key] + if 'postgres' in db_info['ENGINE']: + return cls( + name=db_key, + db_name=db_info['NAME'], + username=db_info['USER'], + password=db_info['PASSWORD'], + host=db_info['HOST'], + port=db_info['PORT'], + ) + + def save(self, **kwargs): + db_name = self.name + # invalidate cached engine as db credentials might get changed. + if _engine_cache.get(db_name): + _engine_cache[db_name].dispose() + del _engine_cache[db_name] + return super().save() + class Schema(DatabaseObject): database = models.ForeignKey('Database', on_delete=models.CASCADE, @@ -476,7 +511,6 @@ def sa_num_records(self, filter=None, search=None): def update_sa_table(self, update_params): result = model_utils.update_sa_table(self, update_params) - reset_reflection(db_name=self.schema.database.name) return result def delete_sa_table(self): @@ -762,6 +796,10 @@ def name(self): else: return name + @property + def description(self): + return get_column_description(self.table.oid, self.attnum, self._sa_engine) + @property def ui_type(self): if self.db_type: @@ -871,6 +909,7 @@ class DataFile(BaseModel): base_name = models.CharField(max_length=100) header = models.BooleanField(default=True) max_level = models.IntegerField(default=0, blank=True) + sheet_index = models.IntegerField(default=0) delimiter = models.CharField(max_length=1, default=',', blank=True) escapechar = models.CharField(max_length=1, blank=True) quotechar = models.CharField(max_length=1, default='"', blank=True) @@ -881,10 +920,26 @@ class PreviewColumnSettings(BaseModel): template = models.CharField(max_length=255) +def validate_column_order(value): + """ + Custom validator to ensure that all elements in the list are positive integers. + """ + if not all(isinstance(item, int) and item > 0 for item in value): + raise ValidationError("All elements of column order must be positive integers.") + + class TableSettings(ReflectionManagerMixin, BaseModel): preview_settings = models.OneToOneField(PreviewColumnSettings, on_delete=models.CASCADE) table = models.OneToOneField(Table, on_delete=models.CASCADE, related_name="settings") - column_order = ArrayField(models.IntegerField(), null=True, default=None) + column_order = JSONField(null=True, blank=True, default=None, validators=[validate_column_order]) + + def save(self, **kwargs): + # Cleans the fields before saving by running respective field validator(s) + try: + self.clean_fields() + except ValidationError as e: + raise e + super().save(**kwargs) def _create_table_settings(tables): diff --git a/mathesar/models/users.py b/mathesar/models/users.py index 5bcae308bc..9e3fcbe86a 100644 --- a/mathesar/models/users.py +++ b/mathesar/models/users.py @@ -15,6 +15,7 @@ class User(AbstractUser): full_name = models.CharField(max_length=255, blank=True, null=True) short_name = models.CharField(max_length=255, blank=True, null=True) password_change_needed = models.BooleanField(default=False) + display_language = models.CharField(max_length=30, blank=True, default='en') class Role(models.TextChoices): diff --git a/mathesar/start.py b/mathesar/start.py new file mode 100644 index 0000000000..6ebc452983 --- /dev/null +++ b/mathesar/start.py @@ -0,0 +1,7 @@ +import sys +import gunicorn.app.wsgiapp as wsgi +from mathesar.install import main as run_install +# This is just a simple way to supply args to gunicorn +sys.argv = [".", "config.wsgi", "--bind=0.0.0.0:8000"] +run_install(skip_static_collection=True) +wsgi.run() diff --git a/mathesar/state/django.py b/mathesar/state/django.py index 9e296f7ba9..16b0dd2ba4 100644 --- a/mathesar/state/django.py +++ b/mathesar/state/django.py @@ -76,7 +76,7 @@ def _set_db_is_deleted(db, deleted): # TODO pass in a cached engine instead of creating a new one def reflect_schemas_from_database(database): - engine = create_mathesar_engine(database.name) + engine = create_mathesar_engine(database) db_schema_oids = { schema['oid'] for schema in get_mathesar_schemas_with_oids(engine) } @@ -181,7 +181,7 @@ def _delete_stale_columns(attnum_tuples, tables): # TODO pass in a cached engine instead of creating a new one def reflect_constraints_from_database(database): - engine = create_mathesar_engine(database.name) + engine = create_mathesar_engine(database) db_constraints = get_constraints_with_oids(engine) map_of_table_oid_to_constraint_oids = defaultdict(list) for db_constraint in db_constraints: @@ -221,7 +221,7 @@ def _delete_stale_dj_constraints(known_db_constraints, database): # TODO pass in a cached engine instead of creating a new one def reflect_new_table_constraints(table): - engine = create_mathesar_engine(table.schema.database.name) + engine = create_mathesar_engine(table.schema.database) db_constraints = get_constraints_with_oids(engine, table_oid=table.oid) constraints = [ models.Constraint.current_objects.get_or_create( diff --git a/mathesar/templates/mathesar/index.html b/mathesar/templates/mathesar/index.html index e26130095d..30e8593603 100644 --- a/mathesar/templates/mathesar/index.html +++ b/mathesar/templates/mathesar/index.html @@ -4,9 +4,11 @@ {% block title %}Home{% endblock %} {% block styles %} - {% if not development_mode %} {% for css_file in manifest_data.module_css %} - - {% endfor %} {% endif %} + {% if not development_mode %} + {% for css_file in manifest_data.module_css %} + + {% endfor %} + {% endif %} {% endblock %} {% block scripts %} @@ -17,6 +19,8 @@ {% endfor %} {% endif %} + + {% if development_mode %} @@ -52,12 +56,22 @@ > + + {% endif %} {% endblock %} diff --git a/mathesar/templates/mathesar/login_base.html b/mathesar/templates/mathesar/login_base.html index f631ab17fd..b656680587 100644 --- a/mathesar/templates/mathesar/login_base.html +++ b/mathesar/templates/mathesar/login_base.html @@ -1,7 +1,7 @@ {% extends 'mathesar/app_styled_base.html' %} {% load i18n static %} -{% block title %}{% translate "login"|title %}{% endblock %} +{% block title %}{% translate "Login" %}{% endblock %} {% block page_styles %} diff --git a/mathesar_ui/src/component-library/radio-group/RadioGroup.svelte b/mathesar_ui/src/component-library/radio-group/RadioGroup.svelte index d5a678938a..b561b3d868 100644 --- a/mathesar_ui/src/component-library/radio-group/RadioGroup.svelte +++ b/mathesar_ui/src/component-library/radio-group/RadioGroup.svelte @@ -7,12 +7,13 @@ export let value: Option | undefined = undefined; export let isInline = false; - export let options: Option[] = []; + export let options: readonly Option[] = []; export let label: string | undefined = undefined; export let ariaLabel: string | undefined = undefined; export let radioLabelKey: string | undefined = undefined; export let getRadioLabel: LabelGetter + + + + + + {#if isSuperUser} + + {/if} + + + + + + diff --git a/mathesar_ui/src/pages/connections/ConnectionsPage.svelte b/mathesar_ui/src/pages/connections/ConnectionsPage.svelte new file mode 100644 index 0000000000..b74378f7e3 --- /dev/null +++ b/mathesar_ui/src/pages/connections/ConnectionsPage.svelte @@ -0,0 +1,175 @@ + + + + {makeSimplePageTitle($_('connections'))} + + + +
+ + {$_('database_connections')} + {#if $connections.size}({$connections.size}){/if} + +
+ +
+ {#if $connections.size === 0} + + {:else} + + + {#if isSuperUser} + + {/if} + + +

+ + {#if slotName === 'searchValue'} + {filterQuery} + {/if} + +

+ + + {#if filteredConnections.length} +
+
+ + + {connection.nickname} + + {connection.database}{connection.username}{connection.host}{connection.port} +
+ + +
+
+ + + + + + + + {#if isSuperUser} + + {/if} + + + + {#each filteredConnections as connection (connection.id)} + + {/each} + +
{$_('connection_name')}{$_('database_name')}{$_('username')}{$_('host')}{$_('port')}{$_('actions')}
+ + {/if} + + + {/if} + + + + + + diff --git a/mathesar_ui/src/pages/data-explorer/DataExplorerPage.svelte b/mathesar_ui/src/pages/data-explorer/DataExplorerPage.svelte index 6897eb592c..7d4ba28952 100644 --- a/mathesar_ui/src/pages/data-explorer/DataExplorerPage.svelte +++ b/mathesar_ui/src/pages/data-explorer/DataExplorerPage.svelte @@ -1,4 +1,5 @@ - {makeSimplePageTitle($query.name ?? 'Data Explorer')} + {makeSimplePageTitle($query.name ?? $_('data_explorer'))} diff --git a/mathesar_ui/src/pages/database/AddEditSchemaModal.svelte b/mathesar_ui/src/pages/database/AddEditSchemaModal.svelte index 1f7dee9b77..97e582aaa7 100644 --- a/mathesar_ui/src/pages/database/AddEditSchemaModal.svelte +++ b/mathesar_ui/src/pages/database/AddEditSchemaModal.svelte @@ -1,5 +1,6 @@ - -

- - diff --git a/mathesar_ui/src/pages/database/DatabasePage.svelte b/mathesar_ui/src/pages/database/DatabasePage.svelte index faaa7b43fe..7f8be0300e 100644 --- a/mathesar_ui/src/pages/database/DatabasePage.svelte +++ b/mathesar_ui/src/pages/database/DatabasePage.svelte @@ -1,29 +1,25 @@ -{makeSimplePageTitle(database.name)} + + {makeSimplePageTitle(database.nickname)} + - - - {#key database.id} - - {/key} - + {#key database.id} + + {/key} diff --git a/mathesar_ui/src/pages/database/DbAccessControlModal.svelte b/mathesar_ui/src/pages/database/DbAccessControlModal.svelte index 56b6aaccf8..3657c8bd58 100644 --- a/mathesar_ui/src/pages/database/DbAccessControlModal.svelte +++ b/mathesar_ui/src/pages/database/DbAccessControlModal.svelte @@ -1,10 +1,12 @@

- {labeledCount(schema.num_tables, 'tables')} + {$_('count_tables', { values: { count: schema.num_tables } })}

- {labeledCount(schema.num_queries, 'explorations')} + {$_('count_explorations', { values: { count: schema.num_queries } })}

diff --git a/mathesar_ui/src/pages/database/SchemaListSkeleton.svelte b/mathesar_ui/src/pages/database/SchemaListSkeleton.svelte new file mode 100644 index 0000000000..a87e889ae8 --- /dev/null +++ b/mathesar_ui/src/pages/database/SchemaListSkeleton.svelte @@ -0,0 +1,29 @@ + + +
+ {#each skeletons as _ (_)} +
+ +
+ {/each} +
+ + diff --git a/mathesar_ui/src/pages/database/SchemaRow.svelte b/mathesar_ui/src/pages/database/SchemaRow.svelte index 57883d7b51..e732fb398f 100644 --- a/mathesar_ui/src/pages/database/SchemaRow.svelte +++ b/mathesar_ui/src/pages/database/SchemaRow.svelte @@ -1,6 +1,6 @@ @@ -51,7 +51,7 @@ menuStyle="--spacing-y:0.8em;" > dispatch('edit')} icon={iconEdit}> - Edit Schema + {$_('edit_schema')} dispatch('delete')} icon={iconDeleteMajor} > - Delete Schema + {$_('delete_schema')} @@ -76,8 +76,7 @@ {#if isDefault} - Every PostgreSQL database includes the "public" schema. This protected - schema can be read by anybody who accesses the database. + {$_('public_schema_info')} {/if} @@ -125,7 +124,8 @@ box-shadow: 0 0.2rem 0.4rem 0 rgba(0, 0, 0, 0.1); } .schema-row.focus { - outline: 1px solid var(--slate-300); + outline: 2px solid var(--slate-300); + outline-offset: 1px; } .schema-row.is-locked { diff --git a/mathesar_ui/src/pages/database/__help__/databaseHelp.ts b/mathesar_ui/src/pages/database/__help__/databaseHelp.ts deleted file mode 100644 index 3cfc5f5e80..0000000000 --- a/mathesar_ui/src/pages/database/__help__/databaseHelp.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const deleteSchemaConfirmationBody = [ - 'All objects in this schema will be deleted permanently, including (but not limited to) tables and views. Some of these objects may not be visible in the Mathesar UI.', - 'Are you sure you want to proceed?', -]; diff --git a/mathesar_ui/src/pages/exploration/ExplorationPage.svelte b/mathesar_ui/src/pages/exploration/ExplorationPage.svelte index 2810b9ee6f..b71566c299 100644 --- a/mathesar_ui/src/pages/exploration/ExplorationPage.svelte +++ b/mathesar_ui/src/pages/exploration/ExplorationPage.svelte @@ -1,6 +1,6 @@ - {query.name} | {schema.name} | Mathesar + {query.name} | {schema.name} | {$_('mathesar')} diff --git a/mathesar_ui/src/pages/exploration/Header.svelte b/mathesar_ui/src/pages/exploration/Header.svelte index 024bbc2108..6c5d771269 100644 --- a/mathesar_ui/src/pages/exploration/Header.svelte +++ b/mathesar_ui/src/pages/exploration/Header.svelte @@ -1,4 +1,5 @@ - -{makeSimplePageTitle('Import')} - - -
- -

Finish setting up your table

- - {#if tableIsAlreadyConfirmed} - Table has already been confirmed. Click here to view the table. - {:else} -
- -

Table Name

- -
- -
- - - -
- -
-

Column names and data types

-

- Column names and data types are automatically detected, use the - controls in the preview table to review and update them if - necessary. -

- {#if isLoading} - - Please wait while we prepare a preview for you - - - {:else if previewRequestStatus?.state === 'failure'} - onPreviewTableIdChange(previewTableId)} - on:delete={handleCancel} - /> - {:else if headerUpdateRequestStatus?.state === 'failure'} - - {:else} - - Preview data is shown for the first few rows of your data only. - - {/if} -
-
- {/if} -
- - {#if !tableIsAlreadyConfirmed} - {#if processedColumns.length > 0} -
-
-

Table Preview

-
-
- c.id} - > - - {#each processedColumns as processedColumn (processedColumn.id)} - -
- - -
-
- {/each} -
- - {#each records as record (record)} - -
- {#each processedColumns as processedColumn (processedColumn)} - -
- -
-
- {/each} -
-
- {/each} -
-
-
-
-
- {/if} - - - {/if} -
-
- - diff --git a/mathesar_ui/src/pages/import-upload/ImportUploadPage.svelte b/mathesar_ui/src/pages/import-upload/ImportUploadPage.svelte deleted file mode 100644 index 079d4be471..0000000000 --- a/mathesar_ui/src/pages/import-upload/ImportUploadPage.svelte +++ /dev/null @@ -1,233 +0,0 @@ - - - - {makeSimplePageTitle($LL.general.import())} - - - -

{$LL.importUploadPage.createATableByImporting()}

-
- {#if isLoading || isError} -
- {$LL.importUploadPage.uploadingData()} - - {$LL.importUploadPage.largeDataTakesTimeWarning()} - -
- {:else} -
- ({ - component: NameWithIcon, - props: { - name: opt.label, - icon: opt.icon, - }, - })} - /> -
- {/if} - -
- { - uploadStatus = { state: 'processing' }; - }} - on:success={(e) => createPreviewTable(e.detail)} - on:error={(e) => { - uploadStatus = { - state: 'failure', - errors: [e.detail ?? $LL.importUploadPage.failedToImport()], - }; - }} - showCancelButton={isError} - on:cancel={() => { - uploadStatus = undefined; - tableCreationProgress = undefined; - }} - hideAllActions={tableCreationProgress?.state === 'processing'} - > - {#if tableCreationProgress?.state === 'processing'} -
- -
- {/if} - - {#if errorMessage} -
- - {$LL.importUploadPage.failedToImport()} - {errorMessage} - -
- {/if} -
-
-
-
- - diff --git a/mathesar_ui/src/pages/import-upload/UploadViaClipboard.svelte b/mathesar_ui/src/pages/import-upload/UploadViaClipboard.svelte deleted file mode 100644 index 285af77736..0000000000 --- a/mathesar_ui/src/pages/import-upload/UploadViaClipboard.svelte +++ /dev/null @@ -1,58 +0,0 @@ - - - -