mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 06:23:34 -07:00
route refactoring
This commit is contained in:
parent
7188e58f4c
commit
d0cc0a089a
15 changed files with 116 additions and 66 deletions
|
@ -90,8 +90,8 @@ app.include_router(static_routes.router)
|
|||
|
||||
|
||||
# Generate API Documentation
|
||||
if not PRODUCTION:
|
||||
generate_api_docs(app)
|
||||
# if not PRODUCTION:
|
||||
# generate_api_docs(app)
|
||||
|
||||
start_scheduler()
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ class BaseDocument:
|
|||
|
||||
def get_all_limit_columns(
|
||||
self, session: Session, fields: List[str], limit: int = None
|
||||
) -> list[SqlAlchemyBase]:
|
||||
) -> List[SqlAlchemyBase]:
|
||||
"""Queries the database for the selected model. Restricts return responses to the
|
||||
keys specified under "fields"
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ from sqlalchemy.orm.session import Session
|
|||
from starlette.responses import FileResponse
|
||||
from utils.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(prefix="/api/backups", tags=["Import / Export"])
|
||||
router = APIRouter(prefix="/api/backups", tags=["Backups"])
|
||||
|
||||
|
||||
@router.get("/available", response_model=Imports)
|
||||
|
|
|
@ -65,7 +65,7 @@ def delete_meal_plan(plan_id, db: Session = Depends(generate_session)):
|
|||
return SnackResponse.success("Mealplan Deleted")
|
||||
|
||||
|
||||
@router.get("/today/", tags=["Meal Plan"])
|
||||
@router.get("/today", tags=["Meal Plan"])
|
||||
def get_today(db: Session = Depends(generate_session)):
|
||||
"""
|
||||
Returns the recipe slug for the meal scheduled for today.
|
||||
|
|
|
@ -88,7 +88,7 @@ def normalize_time(time_entry) -> str:
|
|||
|
||||
def normalize_data(recipe_data: dict) -> dict:
|
||||
recipe_data["totalTime"] = normalize_time(recipe_data.get("totalTime"))
|
||||
recipe_data["description"] = cleanhtml(recipe_data.get("description"))
|
||||
recipe_data["description"] = cleanhtml(recipe_data.get("description", ""))
|
||||
recipe_data["prepTime"] = normalize_time(recipe_data.get("prepTime"))
|
||||
recipe_data["performTime"] = normalize_time(recipe_data.get("performTime"))
|
||||
recipe_data["recipeYield"] = normalize_yield(recipe_data.get("recipeYield"))
|
||||
|
|
|
@ -141,6 +141,8 @@ def default_settings_init():
|
|||
default_entry = SiteSettings(name="main", webhooks=webhooks)
|
||||
document = db.settings.save_new(session, default_entry.dict(), webhooks.dict())
|
||||
|
||||
session.close()
|
||||
|
||||
|
||||
if not sql_exists:
|
||||
default_settings_init()
|
||||
|
|
|
@ -23,6 +23,8 @@ def override_get_db():
|
|||
db.close()
|
||||
|
||||
|
||||
|
||||
|
||||
@fixture(scope="session")
|
||||
def api_client():
|
||||
|
||||
|
|
|
@ -2,6 +2,13 @@ import json
|
|||
|
||||
import pytest
|
||||
from tests.test_routes.utils.routes_data import recipe_test_data
|
||||
from tests.utils.routes import (
|
||||
MEALPLAN_ALL,
|
||||
MEALPLAN_CREATE,
|
||||
MEALPLAN_PREFIX,
|
||||
RECIPES_CREATE_URL,
|
||||
RECIPES_PREFIX,
|
||||
)
|
||||
|
||||
|
||||
def get_meal_plan_template(first=None, second=None):
|
||||
|
@ -23,30 +30,30 @@ def get_meal_plan_template(first=None, second=None):
|
|||
}
|
||||
|
||||
|
||||
## Meal Routes
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def slug_1(api_client):
|
||||
# Slug 1
|
||||
slug_1 = api_client.post(
|
||||
"/api/recipe/create-url/", json={"url": recipe_test_data[0].url}
|
||||
)
|
||||
|
||||
slug_1 = api_client.post(RECIPES_CREATE_URL, json={"url": recipe_test_data[0].url})
|
||||
slug_1 = json.loads(slug_1.content)
|
||||
|
||||
yield slug_1
|
||||
|
||||
api_client.delete(f"/api/recipe/{recipe_test_data[1].expected_slug}/delete/")
|
||||
api_client.delete(RECIPES_PREFIX + "/" + slug_1)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def slug_2(api_client):
|
||||
# Slug 2
|
||||
slug_2 = api_client.post(
|
||||
"/api/recipe/create-url/", json={"url": recipe_test_data[1].url}
|
||||
)
|
||||
slug_2 = api_client.post(RECIPES_CREATE_URL, json={"url": recipe_test_data[1].url})
|
||||
slug_2 = json.loads(slug_2.content)
|
||||
|
||||
yield slug_2
|
||||
|
||||
api_client.delete(f"/api/recipe/{recipe_test_data[0].expected_slug}/delete/")
|
||||
api_client.delete(RECIPES_PREFIX + "/" + slug_2)
|
||||
|
||||
|
||||
def test_create_mealplan(api_client, slug_1, slug_2):
|
||||
|
@ -54,12 +61,12 @@ def test_create_mealplan(api_client, slug_1, slug_2):
|
|||
meal_plan["meals"][0]["slug"] = slug_1
|
||||
meal_plan["meals"][1]["slug"] = slug_2
|
||||
|
||||
response = api_client.post("/api/meal-plan/create/", json=meal_plan)
|
||||
response = api_client.post(MEALPLAN_CREATE, json=meal_plan)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_read_mealplan(api_client, slug_1, slug_2):
|
||||
response = api_client.get("/api/meal-plan/all/")
|
||||
response = api_client.get(MEALPLAN_ALL)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
@ -74,7 +81,7 @@ def test_read_mealplan(api_client, slug_1, slug_2):
|
|||
|
||||
def test_update_mealplan(api_client, slug_1, slug_2):
|
||||
|
||||
response = api_client.get("/api/meal-plan/all/")
|
||||
response = api_client.get(MEALPLAN_ALL)
|
||||
|
||||
existing_mealplan = json.loads(response.text)
|
||||
existing_mealplan = existing_mealplan[0]
|
||||
|
@ -84,13 +91,11 @@ def test_update_mealplan(api_client, slug_1, slug_2):
|
|||
existing_mealplan["meals"][0]["slug"] = slug_2
|
||||
existing_mealplan["meals"][1]["slug"] = slug_1
|
||||
|
||||
response = api_client.post(
|
||||
f"/api/meal-plan/{plan_uid}/update/", json=existing_mealplan
|
||||
)
|
||||
response = api_client.put(f"{MEALPLAN_PREFIX}/{plan_uid}", json=existing_mealplan)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
response = api_client.get("/api/meal-plan/all/")
|
||||
response = api_client.get(MEALPLAN_ALL)
|
||||
existing_mealplan = json.loads(response.text)
|
||||
existing_mealplan = existing_mealplan[0]
|
||||
|
||||
|
@ -99,11 +104,13 @@ def test_update_mealplan(api_client, slug_1, slug_2):
|
|||
|
||||
|
||||
def test_delete_mealplan(api_client):
|
||||
response = api_client.get("/api/meal-plan/all/")
|
||||
response = api_client.get(MEALPLAN_ALL)
|
||||
|
||||
assert response.status_code == 200
|
||||
existing_mealplan = json.loads(response.text)
|
||||
existing_mealplan = existing_mealplan[0]
|
||||
|
||||
plan_uid = existing_mealplan.get("uid")
|
||||
response = api_client.delete(f"/api/meal-plan/{plan_uid}/delete/")
|
||||
response = api_client.delete(f"{MEALPLAN_PREFIX}/{plan_uid}")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
|
|
@ -4,6 +4,7 @@ import shutil
|
|||
import pytest
|
||||
from app_config import MIGRATION_DIR
|
||||
from tests.test_config import TEST_CHOWDOWN_DIR, TEST_NEXTCLOUD_DIR
|
||||
from tests.utils.routes import MIGRATIONS_PREFIX, RECIPES_PREFIX
|
||||
|
||||
|
||||
### Chowdown
|
||||
|
@ -23,7 +24,8 @@ def chowdown_zip():
|
|||
def test_upload_chowdown_zip(api_client, chowdown_zip):
|
||||
|
||||
response = api_client.post(
|
||||
"/api/migrations/chowdown/upload/", files={"archive": chowdown_zip.open("rb")}
|
||||
f"{MIGRATIONS_PREFIX}/chowdown/upload",
|
||||
files={"archive": chowdown_zip.open("rb")},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
@ -33,7 +35,7 @@ def test_upload_chowdown_zip(api_client, chowdown_zip):
|
|||
|
||||
def test_import_chowdown_directory(api_client, chowdown_zip):
|
||||
selection = chowdown_zip.name
|
||||
response = api_client.post(f"/api/migrations/chowdown/{selection}/import/")
|
||||
response = api_client.post(f"{MIGRATIONS_PREFIX}/chowdown/{selection}/import")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
@ -41,13 +43,13 @@ def test_import_chowdown_directory(api_client, chowdown_zip):
|
|||
assert report["failed"] == []
|
||||
|
||||
expected_slug = "roasted-okra"
|
||||
response = api_client.get(f"/api/recipe/{expected_slug}/")
|
||||
response = api_client.get(f"{RECIPES_PREFIX}/{expected_slug}")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_delete_chowdown_migration_data(api_client, chowdown_zip):
|
||||
selection = chowdown_zip.name
|
||||
response = api_client.delete(f"/api/migrations/chowdown/{selection}/delete/")
|
||||
response = api_client.delete(f"{MIGRATIONS_PREFIX}/chowdown/{selection}/delete")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert not MIGRATION_DIR.joinpath(chowdown_zip.name).is_file()
|
||||
|
@ -70,7 +72,8 @@ def nextcloud_zip():
|
|||
def test_upload_nextcloud_zip(api_client, nextcloud_zip):
|
||||
|
||||
response = api_client.post(
|
||||
"/api/migrations/nextcloud/upload/", files={"archive": nextcloud_zip.open("rb")}
|
||||
f"{MIGRATIONS_PREFIX}/nextcloud/upload",
|
||||
files={"archive": nextcloud_zip.open("rb")},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
@ -80,7 +83,7 @@ def test_upload_nextcloud_zip(api_client, nextcloud_zip):
|
|||
|
||||
def test_import_nextcloud_directory(api_client, nextcloud_zip):
|
||||
selection = nextcloud_zip.name
|
||||
response = api_client.post(f"/api/migrations/nextcloud/{selection}/import/")
|
||||
response = api_client.post(f"{MIGRATIONS_PREFIX}/nextcloud/{selection}/import")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
@ -88,13 +91,13 @@ def test_import_nextcloud_directory(api_client, nextcloud_zip):
|
|||
assert report["failed"] == []
|
||||
|
||||
expected_slug = "air-fryer-shrimp"
|
||||
response = api_client.get(f"/api/recipe/{expected_slug}/")
|
||||
response = api_client.get(f"{RECIPES_PREFIX}/{expected_slug}")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_delete__nextcloud_migration_data(api_client, nextcloud_zip):
|
||||
selection = nextcloud_zip.name
|
||||
response = api_client.delete(f"/api/migrations/nextcloud/{selection}/delete/")
|
||||
response = api_client.delete(f"{MIGRATIONS_PREFIX}/nextcloud/{selection}/delete")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert not MIGRATION_DIR.joinpath(nextcloud_zip.name).is_file()
|
||||
|
|
|
@ -2,32 +2,31 @@ import json
|
|||
|
||||
import pytest
|
||||
from slugify import slugify
|
||||
from tests.test_routes.utils.routes_data import (
|
||||
RecipeTestData,
|
||||
raw_recipe,
|
||||
from tests.test_routes.utils.routes_data import (RecipeTestData, raw_recipe,
|
||||
raw_recipe_no_image,
|
||||
recipe_test_data,
|
||||
)
|
||||
recipe_test_data)
|
||||
from tests.utils.routes import (RECIPES_ALL, RECIPES_CREATE,
|
||||
RECIPES_CREATE_URL, RECIPES_PREFIX)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("recipe_data", recipe_test_data)
|
||||
def test_create_by_url(api_client, recipe_data: RecipeTestData):
|
||||
response = api_client.post("/api/recipe/create-url/", json={"url": recipe_data.url})
|
||||
response = api_client.post(RECIPES_CREATE_URL, json={"url": recipe_data.url})
|
||||
assert response.status_code == 201
|
||||
assert json.loads(response.text) == recipe_data.expected_slug
|
||||
|
||||
|
||||
def test_create_by_json(api_client):
|
||||
response = api_client.post("/api/recipe/create/", json=raw_recipe)
|
||||
response = api_client.post(RECIPES_CREATE, json=raw_recipe)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 201
|
||||
assert json.loads(response.text) == "banana-bread"
|
||||
|
||||
|
||||
def test_create_no_image(api_client):
|
||||
response = api_client.post("/api/recipe/create/", json=raw_recipe_no_image)
|
||||
response = api_client.post(RECIPES_CREATE, json=raw_recipe_no_image)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 201
|
||||
assert json.loads(response.text) == "banana-bread-no-image"
|
||||
|
||||
|
||||
|
@ -35,24 +34,24 @@ def test_create_no_image(api_client):
|
|||
# data = {"image": test_image.open("rb").read(), "extension": "jpg"}
|
||||
|
||||
# response = api_client.post(
|
||||
# "/api/recipe/banana-bread-no-image/update/image/", files=data
|
||||
# "{RECIPES_PREFIX}banana-bread-no-image/update/image/", files=data
|
||||
# )
|
||||
|
||||
# assert response.status_code == 200
|
||||
|
||||
# response = api_client.get("/api/recipe/banana-bread-no-image/update/image/")
|
||||
# response = api_client.get("{RECIPES_PREFIX}banana-bread-no-image/update/image/")
|
||||
|
||||
|
||||
def test_read_all_post(api_client):
|
||||
response = api_client.post(
|
||||
"/api/all-recipes/", json={"properties": ["slug", "description", "rating"]}
|
||||
RECIPES_ALL, json={"properties": ["slug", "description", "rating"]}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.parametrize("recipe_data", recipe_test_data)
|
||||
def test_read_update(api_client, recipe_data):
|
||||
response = api_client.get(f"/api/recipe/{recipe_data.expected_slug}/")
|
||||
response = api_client.get(f"{RECIPES_PREFIX}/{recipe_data.expected_slug}")
|
||||
assert response.status_code == 200
|
||||
|
||||
recipe = json.loads(response.content)
|
||||
|
@ -66,14 +65,14 @@ def test_read_update(api_client, recipe_data):
|
|||
test_categories = ["one", "two", "three"]
|
||||
recipe["categories"] = test_categories
|
||||
|
||||
response = api_client.post(
|
||||
f"/api/recipe/{recipe_data.expected_slug}/update/", json=recipe
|
||||
response = api_client.put(
|
||||
f"{RECIPES_PREFIX}/{recipe_data.expected_slug}", json=recipe
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.text) == recipe_data.expected_slug
|
||||
|
||||
response = api_client.get(f"/api/recipe/{recipe_data.expected_slug}/")
|
||||
response = api_client.get(f"{RECIPES_PREFIX}/{recipe_data.expected_slug}")
|
||||
|
||||
recipe = json.loads(response.content)
|
||||
|
||||
|
@ -83,7 +82,7 @@ def test_read_update(api_client, recipe_data):
|
|||
|
||||
@pytest.mark.parametrize("recipe_data", recipe_test_data)
|
||||
def test_rename(api_client, recipe_data):
|
||||
response = api_client.get(f"/api/recipe/{recipe_data.expected_slug}/")
|
||||
response = api_client.get(f"{RECIPES_PREFIX}/{recipe_data.expected_slug}")
|
||||
assert response.status_code == 200
|
||||
|
||||
recipe = json.loads(response.content)
|
||||
|
@ -91,8 +90,8 @@ def test_rename(api_client, recipe_data):
|
|||
new_slug = slugify(new_name)
|
||||
recipe["name"] = new_name
|
||||
|
||||
response = api_client.post(
|
||||
f"/api/recipe/{recipe_data.expected_slug}/update/", json=recipe
|
||||
response = api_client.put(
|
||||
f"{RECIPES_PREFIX}/{recipe_data.expected_slug}", json=recipe
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
@ -103,5 +102,5 @@ def test_rename(api_client, recipe_data):
|
|||
|
||||
@pytest.mark.parametrize("recipe_data", recipe_test_data)
|
||||
def test_delete(api_client, recipe_data):
|
||||
response = api_client.delete(f"/api/recipe/{recipe_data.expected_slug}/delete/")
|
||||
response = api_client.delete(f"{RECIPES_PREFIX}/{recipe_data.expected_slug}")
|
||||
assert response.status_code == 200
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import json
|
||||
|
||||
import pytest
|
||||
from tests.utils.routes import (
|
||||
SETTINGS_PREFIX,
|
||||
SETTINGS_UPDATE,
|
||||
THEMES_CREATE,
|
||||
THEMES_PREFIX,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
|
@ -26,7 +32,7 @@ def default_theme(api_client):
|
|||
"error": "#EF5350",
|
||||
},
|
||||
}
|
||||
api_client.post("/api/site-settings/themes/create/", json=default_theme)
|
||||
api_client.post(THEMES_CREATE, json=default_theme)
|
||||
|
||||
return default_theme
|
||||
|
||||
|
@ -48,7 +54,7 @@ def new_theme():
|
|||
|
||||
|
||||
def test_default_settings(api_client, default_settings):
|
||||
response = api_client.get("/api/site-settings/")
|
||||
response = api_client.get(SETTINGS_PREFIX)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
@ -62,47 +68,45 @@ def test_update_settings(api_client, default_settings):
|
|||
"https://test3.url.com",
|
||||
]
|
||||
|
||||
response = api_client.post("/api/site-settings/update/", json=default_settings)
|
||||
response = api_client.put(SETTINGS_UPDATE, json=default_settings)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
response = api_client.get("/api/site-settings/")
|
||||
response = api_client.get(SETTINGS_PREFIX)
|
||||
assert json.loads(response.content) == default_settings
|
||||
|
||||
|
||||
def test_default_theme(api_client, default_theme):
|
||||
response = api_client.get("/api/site-settings/themes/default/")
|
||||
response = api_client.get(f"{THEMES_PREFIX}/default")
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.content) == default_theme
|
||||
|
||||
|
||||
def test_create_theme(api_client, new_theme):
|
||||
|
||||
response = api_client.post("/api/site-settings/themes/create/", json=new_theme)
|
||||
response = api_client.post(THEMES_CREATE, json=new_theme)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = api_client.get(f"/api/site-settings/themes/{new_theme.get('name')}/")
|
||||
response = api_client.get(f"{THEMES_PREFIX}/{new_theme.get('name')}")
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.content) == new_theme
|
||||
|
||||
|
||||
def test_read_all_themes(api_client, default_theme, new_theme):
|
||||
response = api_client.get("/api/site-settings/themes/")
|
||||
response = api_client.get(THEMES_PREFIX)
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.content) == [default_theme, new_theme]
|
||||
|
||||
|
||||
def test_read_theme(api_client, default_theme, new_theme):
|
||||
for theme in [default_theme, new_theme]:
|
||||
response = api_client.get(f"/api/site-settings/themes/{theme.get('name')}/")
|
||||
response = api_client.get(f"{THEMES_PREFIX}/{theme.get('name')}")
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.content) == theme
|
||||
|
||||
|
||||
def test_delete_theme(api_client, default_theme, new_theme):
|
||||
for theme in [default_theme, new_theme]:
|
||||
response = api_client.delete(
|
||||
f"/api/site-settings/themes/{theme.get('name')}/delete/"
|
||||
)
|
||||
response = api_client.delete(f"{THEMES_PREFIX}/{theme.get('name')}")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
|
3
mealie/tests/test_routes/test_tags_categories.py
Normal file
3
mealie/tests/test_routes/test_tags_categories.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
test_
|
0
mealie/tests/utils/__init__.py
Normal file
0
mealie/tests/utils/__init__.py
Normal file
31
mealie/tests/utils/routes.py
Normal file
31
mealie/tests/utils/routes.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
BASE = "/api"
|
||||
|
||||
ALL_RECIPES = BASE + "/recipes"
|
||||
|
||||
RECIPES_PREFIX = BASE + "/recipes"
|
||||
RECIPES_ALL = RECIPES_PREFIX
|
||||
RECIPES_CREATE = RECIPES_PREFIX + "/create"
|
||||
RECIPES_CREATE_URL = RECIPES_PREFIX + "/create-url"
|
||||
|
||||
CATEGORIES_PREFIX = BASE + "/categories"
|
||||
TAGS_PREFIX = BASE + "/tags"
|
||||
|
||||
MEALPLAN_PREFIX = BASE + "/meal-plans"
|
||||
MEALPLAN_ALL = MEALPLAN_PREFIX + "/all"
|
||||
MEALPLAN_CREATE = MEALPLAN_PREFIX + "/create"
|
||||
MEALPLAN_THIS_WEEK = MEALPLAN_PREFIX + "/this-week"
|
||||
MEALPLAN_TODAY = MEALPLAN_PREFIX + "/today"
|
||||
|
||||
SETTINGS_PREFIX = BASE + "/site-settings"
|
||||
SETTINGS_UPDATE = SETTINGS_PREFIX
|
||||
TEST_WEBHOOKS = SETTINGS_PREFIX + "/webhooks/test"
|
||||
|
||||
THEMES_PREFIX = BASE + "/themes"
|
||||
THEMES_CREATE = THEMES_PREFIX + "/create"
|
||||
|
||||
BACKUPS_PREFIX = BASE + "/backups"
|
||||
BACKUPS_AVAILABLE = BACKUPS_PREFIX + "/available"
|
||||
BACKUPS_EXPORT = BACKUPS_PREFIX + "/export/database"
|
||||
BACKUPS_UPLOAD = BACKUPS_PREFIX + "/upload"
|
||||
|
||||
MIGRATIONS_PREFIX = BASE + "/migrations"
|
Loading…
Add table
Add a link
Reference in a new issue