Split out a BaseAppCache, make AppCache borg again, add _meta.app_cache
This commit is contained in:
parent
941d23e548
commit
104ad0504b
|
@ -1,6 +1,6 @@
|
||||||
from django.db.backends.schema import BaseDatabaseSchemaEditor
|
from django.db.backends.schema import BaseDatabaseSchemaEditor
|
||||||
from django.db.models.loading import cache, default_cache, AppCache
|
|
||||||
from django.db.models.fields.related import ManyToManyField
|
from django.db.models.fields.related import ManyToManyField
|
||||||
|
from django.db.models.loading import BaseAppCache
|
||||||
|
|
||||||
|
|
||||||
class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
|
@ -38,20 +38,19 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
for field in delete_fields:
|
for field in delete_fields:
|
||||||
del body[field.name]
|
del body[field.name]
|
||||||
del mapping[field.column]
|
del mapping[field.column]
|
||||||
|
# Work inside a new AppCache
|
||||||
|
app_cache = BaseAppCache()
|
||||||
# Construct a new model for the new state
|
# Construct a new model for the new state
|
||||||
meta_contents = {
|
meta_contents = {
|
||||||
'app_label': model._meta.app_label,
|
'app_label': model._meta.app_label,
|
||||||
'db_table': model._meta.db_table + "__new",
|
'db_table': model._meta.db_table + "__new",
|
||||||
'unique_together': model._meta.unique_together if override_uniques is None else override_uniques,
|
'unique_together': model._meta.unique_together if override_uniques is None else override_uniques,
|
||||||
|
'app_cache': app_cache,
|
||||||
}
|
}
|
||||||
meta = type("Meta", tuple(), meta_contents)
|
meta = type("Meta", tuple(), meta_contents)
|
||||||
body['Meta'] = meta
|
body['Meta'] = meta
|
||||||
body['__module__'] = model.__module__
|
body['__module__'] = model.__module__
|
||||||
self.app_cache = AppCache()
|
|
||||||
cache.set_cache(self.app_cache)
|
|
||||||
cache.copy_from(default_cache)
|
|
||||||
temp_model = type(model._meta.object_name, model.__bases__, body)
|
temp_model = type(model._meta.object_name, model.__bases__, body)
|
||||||
cache.set_cache(default_cache)
|
|
||||||
# Create a new table with that format
|
# Create a new table with that format
|
||||||
self.create_model(temp_model)
|
self.create_model(temp_model)
|
||||||
# Copy data from the old table
|
# Copy data from the old table
|
||||||
|
@ -117,9 +116,9 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
|
||||||
return self._alter_many_to_many(model, old_field, new_field, strict)
|
return self._alter_many_to_many(model, old_field, new_field, strict)
|
||||||
elif old_type is None or new_type is None:
|
elif old_type is None or new_type is None:
|
||||||
raise ValueError("Cannot alter field %s into %s - they are not compatible types (probably means only one is an M2M with implicit through model)" % (
|
raise ValueError("Cannot alter field %s into %s - they are not compatible types (probably means only one is an M2M with implicit through model)" % (
|
||||||
old_field,
|
old_field,
|
||||||
new_field,
|
new_field,
|
||||||
))
|
))
|
||||||
# Alter by remaking table
|
# Alter by remaking table
|
||||||
self._remake_table(model, alter_fields=[(old_field, new_field)])
|
self._remake_table(model, alter_fields=[(old_field, new_field)])
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@ from django.db.models.query_utils import DeferredAttribute, deferred_class_facto
|
||||||
from django.db.models.deletion import Collector
|
from django.db.models.deletion import Collector
|
||||||
from django.db.models.options import Options
|
from django.db.models.options import Options
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
from django.db.models.loading import register_models, get_model
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.functional import curry
|
from django.utils.functional import curry
|
||||||
from django.utils.encoding import force_str, force_text
|
from django.utils.encoding import force_str, force_text
|
||||||
|
@ -134,7 +133,7 @@ class ModelBase(type):
|
||||||
new_class._base_manager = new_class._base_manager._copy_to_model(new_class)
|
new_class._base_manager = new_class._base_manager._copy_to_model(new_class)
|
||||||
|
|
||||||
# Bail out early if we have already created this class.
|
# Bail out early if we have already created this class.
|
||||||
m = get_model(new_class._meta.app_label, name,
|
m = new_class._meta.app_cache.get_model(new_class._meta.app_label, name,
|
||||||
seed_cache=False, only_installed=False)
|
seed_cache=False, only_installed=False)
|
||||||
if m is not None:
|
if m is not None:
|
||||||
return m
|
return m
|
||||||
|
@ -242,16 +241,13 @@ class ModelBase(type):
|
||||||
|
|
||||||
new_class._prepare()
|
new_class._prepare()
|
||||||
|
|
||||||
if new_class._meta.auto_register:
|
new_class._meta.app_cache.register_models(new_class._meta.app_label, new_class)
|
||||||
register_models(new_class._meta.app_label, new_class)
|
# Because of the way imports happen (recursively), we may or may not be
|
||||||
# Because of the way imports happen (recursively), we may or may not be
|
# the first time this model tries to register with the framework. There
|
||||||
# the first time this model tries to register with the framework. There
|
# should only be one class for each model, so we always return the
|
||||||
# should only be one class for each model, so we always return the
|
# registered version.
|
||||||
# registered version.
|
return new_class._meta.app_cache.get_model(new_class._meta.app_label, name,
|
||||||
return get_model(new_class._meta.app_label, name,
|
seed_cache=False, only_installed=False)
|
||||||
seed_cache=False, only_installed=False)
|
|
||||||
else:
|
|
||||||
return new_class
|
|
||||||
|
|
||||||
def copy_managers(cls, base_managers):
|
def copy_managers(cls, base_managers):
|
||||||
# This is in-place sorting of an Options attribute, but that's fine.
|
# This is in-place sorting of an Options attribute, but that's fine.
|
||||||
|
|
|
@ -16,57 +16,52 @@ __all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models',
|
||||||
'load_app', 'app_cache_ready')
|
'load_app', 'app_cache_ready')
|
||||||
|
|
||||||
|
|
||||||
class AppCache(object):
|
def _initialize():
|
||||||
|
"""
|
||||||
|
Returns a dictionary to be used as the initial value of the
|
||||||
|
[shared] state of the app cache.
|
||||||
|
"""
|
||||||
|
return dict(
|
||||||
|
# Keys of app_store are the model modules for each application.
|
||||||
|
app_store = SortedDict(),
|
||||||
|
|
||||||
|
# Mapping of installed app_labels to model modules for that app.
|
||||||
|
app_labels = {},
|
||||||
|
|
||||||
|
# Mapping of app_labels to a dictionary of model names to model code.
|
||||||
|
# May contain apps that are not installed.
|
||||||
|
app_models = SortedDict(),
|
||||||
|
|
||||||
|
# Mapping of app_labels to errors raised when trying to import the app.
|
||||||
|
app_errors = {},
|
||||||
|
|
||||||
|
# -- Everything below here is only used when populating the cache --
|
||||||
|
loaded = False,
|
||||||
|
handled = {},
|
||||||
|
postponed = [],
|
||||||
|
nesting_level = 0,
|
||||||
|
_get_models_cache = {},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAppCache(object):
|
||||||
"""
|
"""
|
||||||
A cache that stores installed applications and their models. Used to
|
A cache that stores installed applications and their models. Used to
|
||||||
provide reverse-relations and for app introspection (e.g. admin).
|
provide reverse-relations and for app introspection (e.g. admin).
|
||||||
|
|
||||||
|
This provides the base (non-Borg) AppCache class - the AppCache
|
||||||
|
subclass adds borg-like behaviour for the few cases where it's needed,
|
||||||
|
and adds the code that auto-loads from INSTALLED_APPS.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Keys of app_store are the model modules for each application.
|
self.__dict__ = _initialize()
|
||||||
self.app_store = SortedDict()
|
|
||||||
# Mapping of installed app_labels to model modules for that app.
|
|
||||||
self.app_labels = {}
|
|
||||||
# Mapping of app_labels to a dictionary of model names to model code.
|
|
||||||
# May contain apps that are not installed.
|
|
||||||
self.app_models = SortedDict()
|
|
||||||
# Mapping of app_labels to errors raised when trying to import the app.
|
|
||||||
self.app_errors = {}
|
|
||||||
# -- Everything below here is only used when populating the cache --
|
|
||||||
self.loaded = False
|
|
||||||
self.handled = {}
|
|
||||||
self.postponed = []
|
|
||||||
self.nesting_level = 0
|
|
||||||
self._get_models_cache = {}
|
|
||||||
|
|
||||||
def _populate(self):
|
def _populate(self):
|
||||||
"""
|
"""
|
||||||
Fill in all the cache information. This method is threadsafe, in the
|
Stub method - this base class does no auto-loading.
|
||||||
sense that every caller will see the same state upon return, and if the
|
|
||||||
cache is already initialised, it does no work.
|
|
||||||
"""
|
"""
|
||||||
if self.loaded:
|
self.loaded = True
|
||||||
return
|
|
||||||
# Note that we want to use the import lock here - the app loading is
|
|
||||||
# in many cases initiated implicitly by importing, and thus it is
|
|
||||||
# possible to end up in deadlock when one thread initiates loading
|
|
||||||
# without holding the importer lock and another thread then tries to
|
|
||||||
# import something which also launches the app loading. For details of
|
|
||||||
# this situation see #18251.
|
|
||||||
imp.acquire_lock()
|
|
||||||
try:
|
|
||||||
if self.loaded:
|
|
||||||
return
|
|
||||||
for app_name in settings.INSTALLED_APPS:
|
|
||||||
if app_name in self.handled:
|
|
||||||
continue
|
|
||||||
self.load_app(app_name, True)
|
|
||||||
if not self.nesting_level:
|
|
||||||
for app_name in self.postponed:
|
|
||||||
self.load_app(app_name)
|
|
||||||
self.loaded = True
|
|
||||||
finally:
|
|
||||||
imp.release_lock()
|
|
||||||
|
|
||||||
def _label_for(self, app_mod):
|
def _label_for(self, app_mod):
|
||||||
"""
|
"""
|
||||||
|
@ -253,42 +248,58 @@ class AppCache(object):
|
||||||
self.register_models(app_label, *models.values())
|
self.register_models(app_label, *models.values())
|
||||||
|
|
||||||
|
|
||||||
class AppCacheWrapper(object):
|
class AppCache(BaseAppCache):
|
||||||
"""
|
"""
|
||||||
As AppCache can be changed at runtime, this class wraps it so any
|
A cache that stores installed applications and their models. Used to
|
||||||
imported references to 'cache' are changed along with it.
|
provide reverse-relations and for app introspection (e.g. admin).
|
||||||
|
|
||||||
|
Borg version of the BaseAppCache class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, cache):
|
__shared_state = _initialize()
|
||||||
self._cache = cache
|
|
||||||
|
|
||||||
def set_cache(self, cache):
|
def __init__(self):
|
||||||
self._cache = cache
|
self.__dict__ = self.__shared_state
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def _populate(self):
|
||||||
if attr in ("_cache", "set_cache"):
|
"""
|
||||||
return self.__dict__[attr]
|
Fill in all the cache information. This method is threadsafe, in the
|
||||||
return getattr(self._cache, attr)
|
sense that every caller will see the same state upon return, and if the
|
||||||
|
cache is already initialised, it does no work.
|
||||||
def __setattr__(self, attr, value):
|
"""
|
||||||
if attr in ("_cache", "set_cache"):
|
if self.loaded:
|
||||||
self.__dict__[attr] = value
|
|
||||||
return
|
return
|
||||||
return setattr(self._cache, attr, value)
|
# Note that we want to use the import lock here - the app loading is
|
||||||
|
# in many cases initiated implicitly by importing, and thus it is
|
||||||
|
# possible to end up in deadlock when one thread initiates loading
|
||||||
|
# without holding the importer lock and another thread then tries to
|
||||||
|
# import something which also launches the app loading. For details of
|
||||||
|
# this situation see #18251.
|
||||||
|
imp.acquire_lock()
|
||||||
|
try:
|
||||||
|
if self.loaded:
|
||||||
|
return
|
||||||
|
for app_name in settings.INSTALLED_APPS:
|
||||||
|
if app_name in self.handled:
|
||||||
|
continue
|
||||||
|
self.load_app(app_name, True)
|
||||||
|
if not self.nesting_level:
|
||||||
|
for app_name in self.postponed:
|
||||||
|
self.load_app(app_name)
|
||||||
|
self.loaded = True
|
||||||
|
finally:
|
||||||
|
imp.release_lock()
|
||||||
|
|
||||||
|
cache = AppCache()
|
||||||
default_cache = AppCache()
|
|
||||||
cache = AppCacheWrapper(default_cache)
|
|
||||||
|
|
||||||
|
|
||||||
# These methods were always module level, so are kept that way for backwards
|
# These methods were always module level, so are kept that way for backwards
|
||||||
# compatibility. These are wrapped with lambdas to stop the attribute
|
# compatibility.
|
||||||
# access resolving directly to a method on a single cache instance.
|
get_apps = cache.get_apps
|
||||||
get_apps = lambda *x, **y: cache.get_apps(*x, **y)
|
get_app = cache.get_app
|
||||||
get_app = lambda *x, **y: cache.get_app(*x, **y)
|
get_app_errors = cache.get_app_errors
|
||||||
get_app_errors = lambda *x, **y: cache.get_app_errors(*x, **y)
|
get_models = cache.get_models
|
||||||
get_models = lambda *x, **y: cache.get_models(*x, **y)
|
get_model = cache.get_model
|
||||||
get_model = lambda *x, **y: cache.get_model(*x, **y)
|
register_models = cache.register_models
|
||||||
register_models = lambda *x, **y: cache.register_models(*x, **y)
|
load_app = cache.load_app
|
||||||
load_app = lambda *x, **y: cache.load_app(*x, **y)
|
app_cache_ready = cache.app_cache_ready
|
||||||
app_cache_ready = lambda *x, **y: cache.app_cache_ready(*x, **y)
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.conf import settings
|
||||||
from django.db.models.fields.related import ManyToManyRel
|
from django.db.models.fields.related import ManyToManyRel
|
||||||
from django.db.models.fields import AutoField, FieldDoesNotExist
|
from django.db.models.fields import AutoField, FieldDoesNotExist
|
||||||
from django.db.models.fields.proxy import OrderWrt
|
from django.db.models.fields.proxy import OrderWrt
|
||||||
from django.db.models.loading import get_models, app_cache_ready
|
from django.db.models.loading import get_models, app_cache_ready, cache
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.datastructures import SortedDict
|
from django.utils.datastructures import SortedDict
|
||||||
|
@ -21,7 +21,7 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|
|
||||||
DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
|
DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
|
||||||
'unique_together', 'permissions', 'get_latest_by',
|
'unique_together', 'permissions', 'get_latest_by',
|
||||||
'order_with_respect_to', 'app_label', 'db_tablespace',
|
'order_with_respect_to', 'app_label', 'db_tablespace',
|
||||||
'abstract', 'managed', 'proxy', 'swappable', 'auto_created', 'index_together', 'auto_register')
|
'abstract', 'managed', 'proxy', 'swappable', 'auto_created', 'index_together', 'app_cache')
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
|
@ -70,8 +70,8 @@ class Options(object):
|
||||||
# from *other* models. Needed for some admin checks. Internal use only.
|
# from *other* models. Needed for some admin checks. Internal use only.
|
||||||
self.related_fkey_lookups = []
|
self.related_fkey_lookups = []
|
||||||
|
|
||||||
# If we should auto-register with the AppCache
|
# A custom AppCache to use, if you're making a separate model set.
|
||||||
self.auto_register = True
|
self.app_cache = cache
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name):
|
def contribute_to_class(self, cls, name):
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models.loading import BaseAppCache
|
||||||
|
|
||||||
# Because we want to test creation and deletion of these as separate things,
|
# Because we want to test creation and deletion of these as separate things,
|
||||||
# these models are all marked as unmanaged and only marked as managed while
|
# these models are all inserted into a separate AppCache so the main test
|
||||||
# a schema test is running.
|
# runner doesn't syncdb them.
|
||||||
|
|
||||||
|
new_app_cache = BaseAppCache()
|
||||||
|
|
||||||
|
|
||||||
class Author(models.Model):
|
class Author(models.Model):
|
||||||
|
@ -10,24 +13,24 @@ class Author(models.Model):
|
||||||
height = models.PositiveIntegerField(null=True, blank=True)
|
height = models.PositiveIntegerField(null=True, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
auto_register = False
|
app_cache = new_app_cache
|
||||||
|
|
||||||
|
|
||||||
class AuthorWithM2M(models.Model):
|
class AuthorWithM2M(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
auto_register = False
|
app_cache = new_app_cache
|
||||||
|
|
||||||
|
|
||||||
class Book(models.Model):
|
class Book(models.Model):
|
||||||
author = models.ForeignKey(Author)
|
author = models.ForeignKey(Author)
|
||||||
title = models.CharField(max_length=100, db_index=True)
|
title = models.CharField(max_length=100, db_index=True)
|
||||||
pub_date = models.DateTimeField()
|
pub_date = models.DateTimeField()
|
||||||
#tags = models.ManyToManyField("Tag", related_name="books")
|
# tags = models.ManyToManyField("Tag", related_name="books")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
auto_register = False
|
app_cache = new_app_cache
|
||||||
|
|
||||||
|
|
||||||
class BookWithM2M(models.Model):
|
class BookWithM2M(models.Model):
|
||||||
|
@ -37,7 +40,7 @@ class BookWithM2M(models.Model):
|
||||||
tags = models.ManyToManyField("Tag", related_name="books")
|
tags = models.ManyToManyField("Tag", related_name="books")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
auto_register = False
|
app_cache = new_app_cache
|
||||||
|
|
||||||
|
|
||||||
class BookWithSlug(models.Model):
|
class BookWithSlug(models.Model):
|
||||||
|
@ -47,7 +50,7 @@ class BookWithSlug(models.Model):
|
||||||
slug = models.CharField(max_length=20, unique=True)
|
slug = models.CharField(max_length=20, unique=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
auto_register = False
|
app_cache = new_app_cache
|
||||||
db_table = "schema_book"
|
db_table = "schema_book"
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,7 +59,7 @@ class Tag(models.Model):
|
||||||
slug = models.SlugField(unique=True)
|
slug = models.SlugField(unique=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
auto_register = False
|
app_cache = new_app_cache
|
||||||
|
|
||||||
|
|
||||||
class TagUniqueRename(models.Model):
|
class TagUniqueRename(models.Model):
|
||||||
|
@ -64,7 +67,7 @@ class TagUniqueRename(models.Model):
|
||||||
slug2 = models.SlugField(unique=True)
|
slug2 = models.SlugField(unique=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
auto_register = False
|
app_cache = new_app_cache
|
||||||
db_table = "schema_tag"
|
db_table = "schema_tag"
|
||||||
|
|
||||||
|
|
||||||
|
@ -73,5 +76,5 @@ class UniqueTest(models.Model):
|
||||||
slug = models.SlugField(unique=False)
|
slug = models.SlugField(unique=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
auto_register = False
|
app_cache = new_app_cache
|
||||||
unique_together = ["year", "slug"]
|
unique_together = ["year", "slug"]
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
import copy
|
|
||||||
import datetime
|
import datetime
|
||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
from django.utils.unittest import skipUnless
|
from django.utils.unittest import skipUnless
|
||||||
from django.db import connection, DatabaseError, IntegrityError
|
from django.db import connection, DatabaseError, IntegrityError
|
||||||
from django.db.models.fields import IntegerField, TextField, CharField, SlugField
|
from django.db.models.fields import IntegerField, TextField, CharField, SlugField
|
||||||
from django.db.models.fields.related import ManyToManyField, ForeignKey
|
from django.db.models.fields.related import ManyToManyField, ForeignKey
|
||||||
from django.db.models.loading import cache, default_cache, AppCache
|
|
||||||
from .models import Author, AuthorWithM2M, Book, BookWithSlug, BookWithM2M, Tag, TagUniqueRename, UniqueTest
|
from .models import Author, AuthorWithM2M, Book, BookWithSlug, BookWithM2M, Tag, TagUniqueRename, UniqueTest
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,14 +25,6 @@ class SchemaTests(TransactionTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# Make sure we're in manual transaction mode
|
# Make sure we're in manual transaction mode
|
||||||
connection.set_autocommit(False)
|
connection.set_autocommit(False)
|
||||||
# The unmanaged models need to be removed after the test in order to
|
|
||||||
# prevent bad interactions with the flush operation in other tests.
|
|
||||||
self.app_cache = AppCache()
|
|
||||||
cache.set_cache(self.app_cache)
|
|
||||||
cache.copy_from(default_cache)
|
|
||||||
for model in self.models:
|
|
||||||
cache.register_models("schema", model)
|
|
||||||
model._prepare()
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
# Delete any tables made for our models
|
# Delete any tables made for our models
|
||||||
|
@ -43,8 +33,6 @@ class SchemaTests(TransactionTestCase):
|
||||||
# Rollback anything that may have happened
|
# Rollback anything that may have happened
|
||||||
connection.rollback()
|
connection.rollback()
|
||||||
connection.set_autocommit(True)
|
connection.set_autocommit(True)
|
||||||
cache.set_cache(default_cache)
|
|
||||||
cache.app_models['schema'] = {} # One M2M gets left in the old cache
|
|
||||||
|
|
||||||
def delete_tables(self):
|
def delete_tables(self):
|
||||||
"Deletes all model tables for our models for a clean test environment"
|
"Deletes all model tables for our models for a clean test environment"
|
||||||
|
|
Loading…
Reference in New Issue