new models, frontend functions, public pages
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.utils.version import get_version
|
||||
|
||||
VERSION = (4, 1, 0, "final", 0)
|
||||
|
||||
__version__ = get_version(VERSION)
|
||||
@@ -0,0 +1,195 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Autocomplete feature for admin panel
|
||||
#
|
||||
import operator
|
||||
from functools import update_wrapper, reduce
|
||||
from typing import Tuple, Dict, Callable # NOQA
|
||||
|
||||
from django.apps import apps
|
||||
from django.http import HttpResponse, HttpResponseNotFound
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils.encoding import smart_str
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.text import get_text_list
|
||||
from django.contrib import admin
|
||||
|
||||
from django_extensions.admin.widgets import ForeignKeySearchInput
|
||||
|
||||
|
||||
class ForeignKeyAutocompleteAdminMixin:
|
||||
"""
|
||||
Admin class for models using the autocomplete feature.
|
||||
|
||||
There are two additional fields:
|
||||
- related_search_fields: defines fields of managed model that
|
||||
have to be represented by autocomplete input, together with
|
||||
a list of target model fields that are searched for
|
||||
input string, e.g.:
|
||||
|
||||
related_search_fields = {
|
||||
'author': ('first_name', 'email'),
|
||||
}
|
||||
|
||||
- related_string_functions: contains optional functions which
|
||||
take target model instance as only argument and return string
|
||||
representation. By default __unicode__() method of target
|
||||
object is used.
|
||||
|
||||
And also an optional additional field to set the limit on the
|
||||
results returned by the autocomplete query. You can set this integer
|
||||
value in your settings file using FOREIGNKEY_AUTOCOMPLETE_LIMIT or
|
||||
you can set this per ForeignKeyAutocompleteAdmin basis. If any value
|
||||
is set the results will not be limited.
|
||||
"""
|
||||
|
||||
related_search_fields = {} # type: Dict[str, Tuple[str]]
|
||||
related_string_functions = {} # type: Dict[str, Callable]
|
||||
autocomplete_limit = getattr(settings, "FOREIGNKEY_AUTOCOMPLETE_LIMIT", None)
|
||||
|
||||
def get_urls(self):
|
||||
from django.urls import path
|
||||
|
||||
def wrap(view):
|
||||
def wrapper(*args, **kwargs):
|
||||
return self.admin_site.admin_view(view)(*args, **kwargs)
|
||||
|
||||
return update_wrapper(wrapper, view)
|
||||
|
||||
return [
|
||||
path(
|
||||
"foreignkey_autocomplete/",
|
||||
wrap(self.foreignkey_autocomplete),
|
||||
name="%s_%s_autocomplete"
|
||||
% (self.model._meta.app_label, self.model._meta.model_name),
|
||||
)
|
||||
] + super().get_urls()
|
||||
|
||||
def foreignkey_autocomplete(self, request):
|
||||
"""
|
||||
Search in the fields of the given related model and returns the
|
||||
result as a simple string to be used by the jQuery Autocomplete plugin
|
||||
"""
|
||||
query = request.GET.get("q", None)
|
||||
app_label = request.GET.get("app_label", None)
|
||||
model_name = request.GET.get("model_name", None)
|
||||
search_fields = request.GET.get("search_fields", None)
|
||||
object_pk = request.GET.get("object_pk", None)
|
||||
|
||||
try:
|
||||
to_string_function = self.related_string_functions[model_name]
|
||||
except KeyError:
|
||||
to_string_function = lambda x: x.__str__()
|
||||
|
||||
if search_fields and app_label and model_name and (query or object_pk):
|
||||
|
||||
def construct_search(field_name):
|
||||
# use different lookup methods depending on the notation
|
||||
if field_name.startswith("^"):
|
||||
return "%s__istartswith" % field_name[1:]
|
||||
elif field_name.startswith("="):
|
||||
return "%s__iexact" % field_name[1:]
|
||||
elif field_name.startswith("@"):
|
||||
return "%s__search" % field_name[1:]
|
||||
else:
|
||||
return "%s__icontains" % field_name
|
||||
|
||||
model = apps.get_model(app_label, model_name)
|
||||
|
||||
queryset = model._default_manager.all()
|
||||
data = ""
|
||||
if query:
|
||||
for bit in query.split():
|
||||
or_queries = [
|
||||
models.Q(
|
||||
**{construct_search(smart_str(field_name)): smart_str(bit)}
|
||||
)
|
||||
for field_name in search_fields.split(",")
|
||||
]
|
||||
other_qs = QuerySet(model)
|
||||
other_qs.query.select_related = queryset.query.select_related
|
||||
other_qs = other_qs.filter(reduce(operator.or_, or_queries))
|
||||
queryset = queryset & other_qs
|
||||
|
||||
additional_filter = self.get_related_filter(model, request)
|
||||
if additional_filter:
|
||||
queryset = queryset.filter(additional_filter)
|
||||
|
||||
if self.autocomplete_limit:
|
||||
queryset = queryset[: self.autocomplete_limit]
|
||||
|
||||
data = "".join(
|
||||
[str("%s|%s\n") % (to_string_function(f), f.pk) for f in queryset]
|
||||
)
|
||||
elif object_pk:
|
||||
try:
|
||||
obj = queryset.get(pk=object_pk)
|
||||
except Exception: # FIXME: use stricter exception checking
|
||||
pass
|
||||
else:
|
||||
data = to_string_function(obj)
|
||||
return HttpResponse(data, content_type="text/plain")
|
||||
return HttpResponseNotFound()
|
||||
|
||||
def get_related_filter(self, model, request):
|
||||
"""
|
||||
Given a model class and current request return an optional Q object
|
||||
that should be applied as an additional filter for autocomplete query.
|
||||
If no additional filtering is needed, this method should return
|
||||
None.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_help_text(self, field_name, model_name):
|
||||
searchable_fields = self.related_search_fields.get(field_name, None)
|
||||
if searchable_fields:
|
||||
help_kwargs = {
|
||||
"model_name": model_name,
|
||||
"field_list": get_text_list(searchable_fields, _("and")),
|
||||
}
|
||||
return (
|
||||
_(
|
||||
"Use the left field to do %(model_name)s lookups "
|
||||
"in the fields %(field_list)s."
|
||||
)
|
||||
% help_kwargs
|
||||
)
|
||||
return ""
|
||||
|
||||
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
||||
"""
|
||||
Override the default widget for Foreignkey fields if they are
|
||||
specified in the related_search_fields class attribute.
|
||||
"""
|
||||
if (
|
||||
isinstance(db_field, models.ForeignKey)
|
||||
and db_field.name in self.related_search_fields
|
||||
):
|
||||
help_text = self.get_help_text(
|
||||
db_field.name, db_field.remote_field.model._meta.object_name
|
||||
)
|
||||
if kwargs.get("help_text"):
|
||||
help_text = str("%s %s") % (kwargs["help_text"], help_text)
|
||||
kwargs["widget"] = ForeignKeySearchInput(
|
||||
db_field.remote_field, self.related_search_fields[db_field.name]
|
||||
)
|
||||
kwargs["help_text"] = help_text
|
||||
return super().formfield_for_dbfield(db_field, request, **kwargs)
|
||||
|
||||
|
||||
class ForeignKeyAutocompleteAdmin(ForeignKeyAutocompleteAdminMixin, admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class ForeignKeyAutocompleteTabularInline(
|
||||
ForeignKeyAutocompleteAdminMixin, admin.TabularInline
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
class ForeignKeyAutocompleteStackedInline(
|
||||
ForeignKeyAutocompleteAdminMixin, admin.StackedInline
|
||||
):
|
||||
pass
|
||||
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.contrib.admin import FieldListFilter
|
||||
from django.contrib.admin.utils import prepare_lookup_value
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class NullFieldListFilter(FieldListFilter):
|
||||
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||
self.lookup_kwarg = "{0}__isnull".format(field_path)
|
||||
super().__init__(field, request, params, model, model_admin, field_path)
|
||||
lookup_choices = self.lookups(request, model_admin)
|
||||
self.lookup_choices = () if lookup_choices is None else list(lookup_choices)
|
||||
|
||||
def expected_parameters(self):
|
||||
return [self.lookup_kwarg]
|
||||
|
||||
def value(self):
|
||||
return self.used_parameters.get(self.lookup_kwarg, None)
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
("1", _("Yes")),
|
||||
("0", _("No")),
|
||||
)
|
||||
|
||||
def choices(self, cl):
|
||||
yield {
|
||||
"selected": self.value() is None,
|
||||
"query_string": cl.get_query_string({}, [self.lookup_kwarg]),
|
||||
"display": _("All"),
|
||||
}
|
||||
for lookup, title in self.lookup_choices:
|
||||
yield {
|
||||
"selected": self.value()
|
||||
== prepare_lookup_value(self.lookup_kwarg, lookup),
|
||||
"query_string": cl.get_query_string(
|
||||
{
|
||||
self.lookup_kwarg: lookup,
|
||||
},
|
||||
[],
|
||||
),
|
||||
"display": title,
|
||||
}
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() is not None:
|
||||
kwargs = {self.lookup_kwarg: self.value()}
|
||||
return queryset.filter(**kwargs)
|
||||
return queryset
|
||||
|
||||
|
||||
class NotNullFieldListFilter(NullFieldListFilter):
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
("0", _("Yes")),
|
||||
("1", _("No")),
|
||||
)
|
||||
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import urllib
|
||||
|
||||
from django import forms
|
||||
from django.contrib.admin.sites import site
|
||||
from django.contrib.admin.widgets import ForeignKeyRawIdWidget
|
||||
from django.template.loader import render_to_string
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import Truncator
|
||||
|
||||
|
||||
class ForeignKeySearchInput(ForeignKeyRawIdWidget):
|
||||
"""
|
||||
Widget for displaying ForeignKeys in an autocomplete search input
|
||||
instead in a <select> box.
|
||||
"""
|
||||
|
||||
# Set in subclass to render the widget with a different template
|
||||
widget_template = None
|
||||
# Set this to the patch of the search view
|
||||
search_path = None
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
js_files = [
|
||||
static("django_extensions/js/jquery.bgiframe.js"),
|
||||
static("django_extensions/js/jquery.ajaxQueue.js"),
|
||||
static("django_extensions/js/jquery.autocomplete.js"),
|
||||
]
|
||||
|
||||
return forms.Media(
|
||||
css={"all": (static("django_extensions/css/jquery.autocomplete.css"),)},
|
||||
js=js_files,
|
||||
)
|
||||
|
||||
def label_for_value(self, value):
|
||||
key = self.rel.get_related_field().name
|
||||
obj = self.rel.model._default_manager.get(**{key: value})
|
||||
|
||||
return Truncator(obj).words(14, truncate="...")
|
||||
|
||||
def __init__(self, rel, search_fields, attrs=None):
|
||||
self.search_fields = search_fields
|
||||
super().__init__(rel, site, attrs)
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
opts = self.rel.model._meta
|
||||
app_label = opts.app_label
|
||||
model_name = opts.object_name.lower()
|
||||
related_url = reverse("admin:%s_%s_changelist" % (app_label, model_name))
|
||||
if not self.search_path:
|
||||
self.search_path = urllib.parse.urljoin(
|
||||
related_url, "foreignkey_autocomplete/"
|
||||
)
|
||||
params = self.url_parameters()
|
||||
if params:
|
||||
url = "?" + "&".join(["%s=%s" % (k, v) for k, v in params.items()])
|
||||
else:
|
||||
url = ""
|
||||
|
||||
if "class" not in attrs:
|
||||
attrs["class"] = "vForeignKeyRawIdAdminField"
|
||||
# Call the TextInput render method directly to have more control
|
||||
output = [forms.TextInput.render(self, name, value, attrs)]
|
||||
|
||||
if value:
|
||||
label = self.label_for_value(value)
|
||||
else:
|
||||
label = ""
|
||||
|
||||
context = {
|
||||
"url": url,
|
||||
"related_url": related_url,
|
||||
"search_path": self.search_path,
|
||||
"search_fields": ",".join(self.search_fields),
|
||||
"app_label": app_label,
|
||||
"model_name": model_name,
|
||||
"label": label,
|
||||
"name": name,
|
||||
}
|
||||
output.append(
|
||||
render_to_string(
|
||||
self.widget_template
|
||||
or (
|
||||
"django_extensions/widgets/%s/%s/foreignkey_searchinput.html"
|
||||
% (app_label, model_name),
|
||||
"django_extensions/widgets/%s/foreignkey_searchinput.html"
|
||||
% app_label,
|
||||
"django_extensions/widgets/foreignkey_searchinput.html",
|
||||
),
|
||||
context,
|
||||
)
|
||||
)
|
||||
output.reverse()
|
||||
|
||||
return mark_safe("".join(output))
|
||||
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DjangoExtensionsConfig(AppConfig):
|
||||
name = "django_extensions"
|
||||
verbose_name = "Django Extensions"
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
|
||||
|
||||
class ModelUserFieldPermissionMixin(UserPassesTestMixin):
|
||||
model_permission_user_field = "user"
|
||||
|
||||
def get_model_permission_user_field(self):
|
||||
return self.model_permission_user_field
|
||||
|
||||
def test_func(self):
|
||||
model_attr = self.get_model_permission_user_field()
|
||||
current_user = self.request.user
|
||||
|
||||
return current_user == getattr(self.get_queryset().first(), model_attr)
|
||||
@@ -0,0 +1,294 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import inspect
|
||||
import sys
|
||||
from abc import abstractmethod, ABCMeta
|
||||
from typing import ( # NOQA
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
|
||||
class BaseCR(metaclass=ABCMeta):
|
||||
"""
|
||||
Abstract base collision resolver. All collision resolvers needs to inherit from this class.
|
||||
To write custom collision resolver you need to overwrite resolve_collisions function.
|
||||
It receives Dict[str, List[str]], where key is model name and values are full model names
|
||||
(full model name means: module + model_name).
|
||||
You should return Dict[str, str], where key is model name and value is full model name.
|
||||
""" # noqa: E501
|
||||
|
||||
@classmethod
|
||||
def get_app_name_and_model(cls, full_model_path): # type: (str) -> Tuple[str, str]
|
||||
model_class = import_string(full_model_path)
|
||||
return model_class._meta.app_config.name, model_class.__name__
|
||||
|
||||
@abstractmethod
|
||||
def resolve_collisions(self, namespace): # type: (Dict[str, List[str]]) -> Dict[str, str]
|
||||
pass
|
||||
|
||||
|
||||
class LegacyCR(BaseCR):
|
||||
"""
|
||||
Default collision resolver.
|
||||
|
||||
Model from last application in alphabetical order is selected.
|
||||
"""
|
||||
|
||||
def resolve_collisions(self, namespace):
|
||||
result = {}
|
||||
for name, models in namespace.items():
|
||||
result[name] = models[-1]
|
||||
return result
|
||||
|
||||
|
||||
class AppsOrderCR(LegacyCR, metaclass=ABCMeta):
|
||||
APP_PRIORITIES = None # type: List[str]
|
||||
|
||||
def resolve_collisions(self, namespace):
|
||||
assert self.APP_PRIORITIES is not None, (
|
||||
"You must define APP_PRIORITIES in your resolver class!"
|
||||
)
|
||||
result = {}
|
||||
for name, models in namespace.items():
|
||||
if len(models) > 0:
|
||||
sorted_models = self._sort_models_depending_on_priorities(models)
|
||||
result[name] = sorted_models[0][1]
|
||||
return result
|
||||
|
||||
def _sort_models_depending_on_priorities(self, models): # type: (List[str]) -> List[Tuple[int, str]]
|
||||
models_with_priorities = []
|
||||
for model in models:
|
||||
try:
|
||||
app_name, _ = self.get_app_name_and_model(model)
|
||||
position = self.APP_PRIORITIES.index(app_name)
|
||||
except (ImportError, ValueError):
|
||||
position = sys.maxsize
|
||||
models_with_priorities.append((position, model))
|
||||
return sorted(models_with_priorities)
|
||||
|
||||
|
||||
class InstalledAppsOrderCR(AppsOrderCR):
|
||||
"""
|
||||
Collision resolver which selects first model from INSTALLED_APPS.
|
||||
You can set your own app priorities list by subclassing him and overwriting APP_PRIORITIES field.
|
||||
This collision resolver will select model from first app on this list.
|
||||
If both app's are absent on this list, resolver will choose model from first app in alphabetical order.
|
||||
""" # noqa: E501
|
||||
|
||||
@property
|
||||
def APP_PRIORITIES(self):
|
||||
from django.conf import settings
|
||||
|
||||
return getattr(settings, "INSTALLED_APPS", [])
|
||||
|
||||
|
||||
class PathBasedCR(LegacyCR, metaclass=ABCMeta):
|
||||
"""
|
||||
Abstract resolver which transforms full model name into alias.
|
||||
To use him you need to overwrite transform_import function
|
||||
which should have one parameter. It will be full model name.
|
||||
It should return valid alias as str instance.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def transform_import(self, module_path): # type: (str) -> str
|
||||
pass
|
||||
|
||||
def resolve_collisions(self, namespace):
|
||||
base_imports = super(PathBasedCR, self).resolve_collisions(namespace)
|
||||
for name, models in namespace.items():
|
||||
if len(models) <= 1:
|
||||
continue
|
||||
for model in models:
|
||||
new_name = self.transform_import(model)
|
||||
assert isinstance(new_name, str), (
|
||||
"result of transform_import must be str!"
|
||||
)
|
||||
base_imports[new_name] = model
|
||||
return base_imports
|
||||
|
||||
|
||||
class FullPathCR(PathBasedCR):
|
||||
"""
|
||||
Collision resolver which transform full model name to alias by changing dots to underscores.
|
||||
He also removes 'models' part of alias, because all models are in models.py files.
|
||||
Model from last application in alphabetical order is selected.
|
||||
""" # noqa: E501
|
||||
|
||||
def transform_import(self, module_path):
|
||||
module, model = module_path.rsplit(".models", 1)
|
||||
module_path = module + model
|
||||
return module_path.replace(".", "_")
|
||||
|
||||
|
||||
class AppNameCR(PathBasedCR, metaclass=ABCMeta):
|
||||
"""
|
||||
Abstract collision resolver which transform pair (app name, model_name) to alias by changing dots to underscores.
|
||||
You must define MODIFICATION_STRING which should be string to format with two keyword arguments:
|
||||
app_name and model_name. For example: "{app_name}_{model_name}".
|
||||
Model from last application in alphabetical order is selected.
|
||||
""" # noqa: E501
|
||||
|
||||
MODIFICATION_STRING = None # type: Optional[str]
|
||||
|
||||
def transform_import(self, module_path):
|
||||
assert self.MODIFICATION_STRING is not None, (
|
||||
"You must define MODIFICATION_STRING in your resolver class!"
|
||||
)
|
||||
app_name, model_name = self.get_app_name_and_model(module_path)
|
||||
app_name = app_name.replace(".", "_")
|
||||
return self.MODIFICATION_STRING.format(app_name=app_name, model_name=model_name)
|
||||
|
||||
|
||||
class AppNamePrefixCR(AppNameCR):
|
||||
"""
|
||||
Collision resolver which transform pair (app name, model_name) to alias "{app_name}_{model_name}".
|
||||
Model from last application in alphabetical order is selected.
|
||||
Result is different than FullPathCR, when model has app_label other than current app.
|
||||
""" # noqa: E501
|
||||
|
||||
MODIFICATION_STRING = "{app_name}_{model_name}"
|
||||
|
||||
|
||||
class AppNameSuffixCR(AppNameCR):
|
||||
"""
|
||||
Collision resolver which transform pair (app name, model_name) to alias "{model_name}_{app_name}"
|
||||
Model from last application in alphabetical order is selected.
|
||||
""" # noqa: E501
|
||||
|
||||
MODIFICATION_STRING = "{model_name}_{app_name}"
|
||||
|
||||
|
||||
class AppNamePrefixCustomOrderCR(AppNamePrefixCR, InstalledAppsOrderCR):
|
||||
"""
|
||||
Collision resolver which is mixin of AppNamePrefixCR and InstalledAppsOrderCR.
|
||||
In case of collisions he sets aliases like AppNamePrefixCR, but sets default model using InstalledAppsOrderCR.
|
||||
""" # noqa: E501
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AppNameSuffixCustomOrderCR(AppNameSuffixCR, InstalledAppsOrderCR):
|
||||
"""
|
||||
Collision resolver which is mixin of AppNameSuffixCR and InstalledAppsOrderCR.
|
||||
In case of collisions he sets aliases like AppNameSuffixCR, but sets default model using InstalledAppsOrderCR.
|
||||
""" # noqa: E501
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class FullPathCustomOrderCR(FullPathCR, InstalledAppsOrderCR):
|
||||
"""
|
||||
Collision resolver which is mixin of FullPathCR and InstalledAppsOrderCR.
|
||||
In case of collisions he sets aliases like FullPathCR, but sets default model using InstalledAppsOrderCR.
|
||||
""" # noqa: E501
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AppLabelCR(PathBasedCR, metaclass=ABCMeta):
|
||||
"""
|
||||
Abstract collision resolver which transform pair (app_label, model_name) to alias.
|
||||
You must define MODIFICATION_STRING which should be string to format with two keyword arguments:
|
||||
app_label and model_name. For example: "{app_label}_{model_name}".
|
||||
This is different from AppNameCR when the app is nested with several level of namespace:
|
||||
Gives sites_Site instead of django_contrib_sites_Site
|
||||
Model from last application in alphabetical order is selected.
|
||||
""" # noqa: E501
|
||||
|
||||
MODIFICATION_STRING = None # type: Optional[str]
|
||||
|
||||
def transform_import(self, module_path):
|
||||
assert self.MODIFICATION_STRING is not None, (
|
||||
"You must define MODIFICATION_STRING in your resolver class!"
|
||||
)
|
||||
model_class = import_string(module_path)
|
||||
app_label, model_name = model_class._meta.app_label, model_class.__name__
|
||||
return self.MODIFICATION_STRING.format(
|
||||
app_label=app_label, model_name=model_name
|
||||
)
|
||||
|
||||
|
||||
class AppLabelPrefixCR(AppLabelCR):
|
||||
"""
|
||||
Collision resolver which transform pair (app_label, model_name) to alias "{app_label}_{model_name}".
|
||||
Model from last application in alphabetical order is selected.
|
||||
""" # noqa: E501
|
||||
|
||||
MODIFICATION_STRING = "{app_label}_{model_name}"
|
||||
|
||||
|
||||
class AppLabelSuffixCR(AppLabelCR):
|
||||
"""
|
||||
Collision resolver which transform pair (app_label, model_name) to alias "{model_name}_{app_label}".
|
||||
Model from last application in alphabetical order is selected.
|
||||
""" # noqa: E501
|
||||
|
||||
MODIFICATION_STRING = "{model_name}_{app_label}"
|
||||
|
||||
|
||||
class CollisionResolvingRunner:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def run_collision_resolver(self, models_to_import):
|
||||
# type: (Dict[str, List[str]]) -> Dict[str, List[Tuple[str, str]]]
|
||||
dictionary_of_names = self._get_dictionary_of_names(models_to_import) # type: Dict[str, str]
|
||||
return self._get_dictionary_of_modules(dictionary_of_names)
|
||||
|
||||
@classmethod
|
||||
def _get_dictionary_of_names(cls, models_to_import): # type: (Dict[str, List[str]]) -> (Dict[str, str])
|
||||
from django.conf import settings
|
||||
|
||||
collision_resolver_class = import_string(
|
||||
getattr(
|
||||
settings,
|
||||
"SHELL_PLUS_MODEL_IMPORTS_RESOLVER",
|
||||
"django_extensions.collision_resolvers.LegacyCR",
|
||||
)
|
||||
)
|
||||
|
||||
cls._assert_is_collision_resolver_class_correct(collision_resolver_class)
|
||||
result = collision_resolver_class().resolve_collisions(models_to_import)
|
||||
cls._assert_is_collision_resolver_result_correct(result)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _assert_is_collision_resolver_result_correct(cls, result):
|
||||
assert isinstance(result, dict), (
|
||||
"Result of resolve_collisions function must be a dict!"
|
||||
)
|
||||
for key, value in result.items():
|
||||
assert isinstance(key, str), (
|
||||
"key in collision resolver result should be str not %s" % key
|
||||
)
|
||||
assert isinstance(value, str), (
|
||||
"value in collision resolver result should be str not %s" % value
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _assert_is_collision_resolver_class_correct(cls, collision_resolver_class):
|
||||
assert inspect.isclass(collision_resolver_class) and issubclass(
|
||||
collision_resolver_class, BaseCR
|
||||
), "SHELL_PLUS_MODEL_IMPORTS_RESOLVER must be subclass of BaseCR!"
|
||||
assert (
|
||||
len(
|
||||
inspect.getfullargspec(collision_resolver_class.resolve_collisions).args
|
||||
)
|
||||
== 2
|
||||
), "resolve_collisions function must take one argument!"
|
||||
|
||||
@classmethod
|
||||
def _get_dictionary_of_modules(cls, dictionary_of_names):
|
||||
# type: (Dict[str, str]) -> Dict[str, List[Tuple[str, str]]]
|
||||
dictionary_of_modules = {} # type: Dict[str, List[Tuple[str, str]]]
|
||||
for alias, model in dictionary_of_names.items():
|
||||
module_path, model_name = model.rsplit(".", 1)
|
||||
dictionary_of_modules.setdefault(module_path, [])
|
||||
dictionary_of_modules[module_path].append((model_name, alias))
|
||||
return dictionary_of_modules
|
||||
@@ -0,0 +1,68 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from io import BytesIO
|
||||
|
||||
import csv
|
||||
import codecs
|
||||
import importlib
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
#
|
||||
# Django compatibility
|
||||
#
|
||||
def load_tag_library(libname):
|
||||
"""
|
||||
Load a templatetag library on multiple Django versions.
|
||||
|
||||
Returns None if the library isn't loaded.
|
||||
"""
|
||||
from django.template.backends.django import get_installed_libraries
|
||||
from django.template.library import InvalidTemplateLibrary
|
||||
|
||||
try:
|
||||
lib = get_installed_libraries()[libname]
|
||||
lib = importlib.import_module(lib).register
|
||||
return lib
|
||||
except (InvalidTemplateLibrary, KeyError):
|
||||
return None
|
||||
|
||||
|
||||
def get_template_setting(template_key, default=None):
|
||||
"""Read template settings"""
|
||||
templates_var = getattr(settings, "TEMPLATES", None)
|
||||
if templates_var:
|
||||
for tdict in templates_var:
|
||||
if template_key in tdict:
|
||||
return tdict[template_key]
|
||||
return default
|
||||
|
||||
|
||||
class UnicodeWriter:
|
||||
"""
|
||||
CSV writer which will write rows to CSV file "f",
|
||||
which is encoded in the given encoding.
|
||||
We are using this custom UnicodeWriter for python versions 2.x
|
||||
"""
|
||||
|
||||
def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
|
||||
self.queue = BytesIO()
|
||||
self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
|
||||
self.stream = f
|
||||
self.encoder = codecs.getincrementalencoder(encoding)()
|
||||
|
||||
def writerow(self, row):
|
||||
self.writer.writerow([s.encode("utf-8") for s in row])
|
||||
# Fetch UTF-8 output from the queue ...
|
||||
data = self.queue.getvalue()
|
||||
data = data.decode("utf-8")
|
||||
# ... and reencode it into the target encoding
|
||||
data = self.encoder.encode(data)
|
||||
# write to the target stream
|
||||
self.stream.write(data)
|
||||
# empty queue
|
||||
self.queue.truncate(0)
|
||||
|
||||
def writerows(self, rows):
|
||||
for row in rows:
|
||||
self.writerow(row)
|
||||
@@ -0,0 +1,3 @@
|
||||
from django import forms
|
||||
|
||||
# place form definition here
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.urls import include, path
|
||||
|
||||
# place app url patterns here
|
||||
@@ -0,0 +1 @@
|
||||
# Create your views here.
|
||||
@@ -0,0 +1,11 @@
|
||||
from django.core.management.base import {{ base_command }}
|
||||
|
||||
|
||||
class Command({{ base_command }}):
|
||||
help = "My shiny new management command."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('sample', nargs='+')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,9 @@
|
||||
from django_extensions.management.jobs import BaseJob
|
||||
|
||||
|
||||
class Job(BaseJob):
|
||||
help = "My sample job."
|
||||
|
||||
def execute(self):
|
||||
# executing empty sample job
|
||||
pass
|
||||
@@ -0,0 +1,3 @@
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
@@ -0,0 +1,638 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Django Extensions additional model fields
|
||||
|
||||
Some fields might require additional dependencies to be installed.
|
||||
"""
|
||||
|
||||
import re
|
||||
import string
|
||||
|
||||
try:
|
||||
import uuid
|
||||
|
||||
HAS_UUID = True
|
||||
except ImportError:
|
||||
HAS_UUID = False
|
||||
|
||||
try:
|
||||
import shortuuid
|
||||
|
||||
HAS_SHORT_UUID = True
|
||||
except ImportError:
|
||||
HAS_SHORT_UUID = False
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.models import DateTimeField, CharField, SlugField, Q, UniqueConstraint
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
|
||||
MAX_UNIQUE_QUERY_ATTEMPTS = getattr(
|
||||
settings, "EXTENSIONS_MAX_UNIQUE_QUERY_ATTEMPTS", 100
|
||||
)
|
||||
|
||||
|
||||
class UniqueFieldMixin:
|
||||
def check_is_bool(self, attrname):
|
||||
if not isinstance(getattr(self, attrname), bool):
|
||||
raise ValueError("'{}' argument must be True or False".format(attrname))
|
||||
|
||||
@staticmethod
|
||||
def _get_fields(model_cls):
|
||||
return [
|
||||
(f, f.model if f.model != model_cls else None)
|
||||
for f in model_cls._meta.get_fields()
|
||||
if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model)
|
||||
]
|
||||
|
||||
def get_queryset(self, model_cls, slug_field):
|
||||
for field, model in self._get_fields(model_cls):
|
||||
if model and field == slug_field:
|
||||
return model._default_manager.all()
|
||||
return model_cls._default_manager.all()
|
||||
|
||||
def find_unique(self, model_instance, field, iterator, *args):
|
||||
# exclude the current model instance from the queryset used in finding
|
||||
# next valid hash
|
||||
queryset = self.get_queryset(model_instance.__class__, field)
|
||||
if model_instance.pk:
|
||||
queryset = queryset.exclude(pk=model_instance.pk)
|
||||
|
||||
# form a kwarg dict used to implement any unique_together constraints
|
||||
kwargs = {}
|
||||
for params in model_instance._meta.unique_together:
|
||||
if self.attname in params:
|
||||
for param in params:
|
||||
kwargs[param] = getattr(model_instance, param, None)
|
||||
|
||||
# for support django 2.2+
|
||||
query = Q()
|
||||
constraints = getattr(model_instance._meta, "constraints", None)
|
||||
if constraints:
|
||||
unique_constraints = filter(
|
||||
lambda c: isinstance(c, UniqueConstraint), constraints
|
||||
)
|
||||
for unique_constraint in unique_constraints:
|
||||
if self.attname in unique_constraint.fields:
|
||||
condition = {
|
||||
field: getattr(model_instance, field, None)
|
||||
for field in unique_constraint.fields
|
||||
if field != self.attname
|
||||
}
|
||||
query &= Q(**condition)
|
||||
|
||||
new = next(iterator)
|
||||
kwargs[self.attname] = new
|
||||
while not new or queryset.filter(query, **kwargs):
|
||||
new = next(iterator)
|
||||
kwargs[self.attname] = new
|
||||
setattr(model_instance, self.attname, new)
|
||||
return new
|
||||
|
||||
|
||||
class AutoSlugField(UniqueFieldMixin, SlugField):
|
||||
"""
|
||||
AutoSlugField
|
||||
|
||||
By default, sets editable=False, blank=True.
|
||||
|
||||
Required arguments:
|
||||
|
||||
populate_from
|
||||
Specifies which field, list of fields, or model method
|
||||
the slug will be populated from.
|
||||
|
||||
populate_from can traverse a ForeignKey relationship
|
||||
by using Django ORM syntax:
|
||||
populate_from = 'related_model__field'
|
||||
|
||||
Optional arguments:
|
||||
|
||||
separator
|
||||
Defines the used separator (default: '-')
|
||||
|
||||
overwrite
|
||||
If set to True, overwrites the slug on every save (default: False)
|
||||
|
||||
slugify_function
|
||||
Defines the function which will be used to "slugify" a content
|
||||
(default: :py:func:`~django.template.defaultfilters.slugify` )
|
||||
|
||||
It is possible to provide custom "slugify" function with
|
||||
the ``slugify_function`` function in a model class.
|
||||
|
||||
``slugify_function`` function in a model class takes priority over
|
||||
``slugify_function`` given as an argument to :py:class:`~AutoSlugField`.
|
||||
|
||||
Example
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# models.py
|
||||
|
||||
from django.db import models
|
||||
|
||||
from django_extensions.db.fields import AutoSlugField
|
||||
|
||||
|
||||
class MyModel(models.Model):
|
||||
def slugify_function(self, content):
|
||||
return content.replace('_', '-').lower()
|
||||
|
||||
title = models.CharField(max_length=42)
|
||||
slug = AutoSlugField(populate_from='title')
|
||||
|
||||
Inspired by SmileyChris' Unique Slugify snippet:
|
||||
https://www.djangosnippets.org/snippets/690/
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("blank", True)
|
||||
kwargs.setdefault("editable", False)
|
||||
|
||||
populate_from = kwargs.pop("populate_from", None)
|
||||
if populate_from is None:
|
||||
raise ValueError("missing 'populate_from' argument")
|
||||
else:
|
||||
self._populate_from = populate_from
|
||||
|
||||
if not callable(populate_from):
|
||||
if not isinstance(populate_from, (list, tuple)):
|
||||
populate_from = (populate_from,)
|
||||
|
||||
if not all(isinstance(e, str) for e in populate_from):
|
||||
raise TypeError(
|
||||
"'populate_from' must be str or list[str] or tuple[str], found `%s`"
|
||||
% populate_from
|
||||
)
|
||||
|
||||
self.slugify_function = kwargs.pop("slugify_function", slugify)
|
||||
self.separator = kwargs.pop("separator", "-")
|
||||
self.overwrite = kwargs.pop("overwrite", False)
|
||||
self.check_is_bool("overwrite")
|
||||
self.overwrite_on_add = kwargs.pop("overwrite_on_add", True)
|
||||
self.check_is_bool("overwrite_on_add")
|
||||
self.allow_duplicates = kwargs.pop("allow_duplicates", False)
|
||||
self.check_is_bool("allow_duplicates")
|
||||
self.max_unique_query_attempts = kwargs.pop(
|
||||
"max_unique_query_attempts", MAX_UNIQUE_QUERY_ATTEMPTS
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _slug_strip(self, value):
|
||||
"""
|
||||
Clean up a slug by removing slug separator characters that occur at
|
||||
the beginning or end of a slug.
|
||||
|
||||
If an alternate separator is used, it will also replace any instances
|
||||
of the default '-' separator with the new separator.
|
||||
"""
|
||||
re_sep = "(?:-|%s)" % re.escape(self.separator)
|
||||
value = re.sub("%s+" % re_sep, self.separator, value)
|
||||
return re.sub(r"^%s+|%s+$" % (re_sep, re_sep), "", value)
|
||||
|
||||
@staticmethod
|
||||
def slugify_func(content, slugify_function):
|
||||
if content:
|
||||
return slugify_function(content)
|
||||
return ""
|
||||
|
||||
def slug_generator(self, original_slug, start):
|
||||
yield original_slug
|
||||
for i in range(start, self.max_unique_query_attempts):
|
||||
slug = original_slug
|
||||
end = "%s%s" % (self.separator, i)
|
||||
end_len = len(end)
|
||||
if self.slug_len and len(slug) + end_len > self.slug_len:
|
||||
slug = slug[: self.slug_len - end_len]
|
||||
slug = self._slug_strip(slug)
|
||||
slug = "%s%s" % (slug, end)
|
||||
yield slug
|
||||
raise RuntimeError(
|
||||
"max slug attempts for %s exceeded (%s)"
|
||||
% (original_slug, self.max_unique_query_attempts)
|
||||
)
|
||||
|
||||
def create_slug(self, model_instance, add):
|
||||
slug = getattr(model_instance, self.attname)
|
||||
use_existing_slug = False
|
||||
if slug and not self.overwrite:
|
||||
# Existing slug and not configured to overwrite - Short-circuit
|
||||
# here to prevent slug generation when not required.
|
||||
use_existing_slug = True
|
||||
|
||||
if self.overwrite_on_add and add:
|
||||
use_existing_slug = False
|
||||
|
||||
if use_existing_slug:
|
||||
return slug
|
||||
|
||||
# get fields to populate from and slug field to set
|
||||
populate_from = self._populate_from
|
||||
if not isinstance(populate_from, (list, tuple)):
|
||||
populate_from = (populate_from,)
|
||||
|
||||
slug_field = model_instance._meta.get_field(self.attname)
|
||||
slugify_function = getattr(
|
||||
model_instance, "slugify_function", self.slugify_function
|
||||
)
|
||||
|
||||
# slugify the original field content and set next step to 2
|
||||
slug_for_field = lambda lookup_value: self.slugify_func(
|
||||
self.get_slug_fields(model_instance, lookup_value),
|
||||
slugify_function=slugify_function,
|
||||
)
|
||||
slug = self.separator.join(map(slug_for_field, populate_from))
|
||||
start = 2
|
||||
|
||||
# strip slug depending on max_length attribute of the slug field
|
||||
# and clean-up
|
||||
self.slug_len = slug_field.max_length
|
||||
if self.slug_len:
|
||||
slug = slug[: self.slug_len]
|
||||
slug = self._slug_strip(slug)
|
||||
original_slug = slug
|
||||
|
||||
if self.allow_duplicates:
|
||||
setattr(model_instance, self.attname, slug)
|
||||
return slug
|
||||
|
||||
return self.find_unique(
|
||||
model_instance, slug_field, self.slug_generator(original_slug, start)
|
||||
)
|
||||
|
||||
def get_slug_fields(self, model_instance, lookup_value):
|
||||
if callable(lookup_value):
|
||||
# A function has been provided
|
||||
return "%s" % lookup_value(model_instance)
|
||||
|
||||
lookup_value_path = lookup_value.split(LOOKUP_SEP)
|
||||
attr = model_instance
|
||||
for elem in lookup_value_path:
|
||||
try:
|
||||
attr = getattr(attr, elem)
|
||||
except AttributeError:
|
||||
raise AttributeError(
|
||||
"value {} in AutoSlugField's 'populate_from' argument {} returned an error - {} has no attribute {}".format( # noqa: E501
|
||||
elem, lookup_value, attr, elem
|
||||
)
|
||||
)
|
||||
if callable(attr):
|
||||
return "%s" % attr()
|
||||
|
||||
return attr
|
||||
|
||||
def pre_save(self, model_instance, add):
|
||||
value = force_str(self.create_slug(model_instance, add))
|
||||
return value
|
||||
|
||||
def get_internal_type(self):
|
||||
return "SlugField"
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
kwargs["populate_from"] = self._populate_from
|
||||
if not self.separator == "-":
|
||||
kwargs["separator"] = self.separator
|
||||
if self.overwrite is not False:
|
||||
kwargs["overwrite"] = True
|
||||
if self.allow_duplicates is not False:
|
||||
kwargs["allow_duplicates"] = True
|
||||
return name, path, args, kwargs
|
||||
|
||||
|
||||
class RandomCharField(UniqueFieldMixin, CharField):
|
||||
"""
|
||||
RandomCharField
|
||||
|
||||
By default, sets editable=False, blank=True, unique=False.
|
||||
|
||||
Required arguments:
|
||||
|
||||
length
|
||||
Specifies the length of the field
|
||||
|
||||
Optional arguments:
|
||||
|
||||
unique
|
||||
If set to True, duplicate entries are not allowed (default: False)
|
||||
|
||||
lowercase
|
||||
If set to True, lowercase the alpha characters (default: False)
|
||||
|
||||
uppercase
|
||||
If set to True, uppercase the alpha characters (default: False)
|
||||
|
||||
include_alpha
|
||||
If set to True, include alpha characters (default: True)
|
||||
|
||||
include_digits
|
||||
If set to True, include digit characters (default: True)
|
||||
|
||||
include_punctuation
|
||||
If set to True, include punctuation characters (default: False)
|
||||
|
||||
keep_default
|
||||
If set to True, keeps the default initialization value (default: False)
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("blank", True)
|
||||
kwargs.setdefault("editable", False)
|
||||
|
||||
self.length = kwargs.pop("length", None)
|
||||
if self.length is None:
|
||||
raise ValueError("missing 'length' argument")
|
||||
kwargs["max_length"] = self.length
|
||||
|
||||
self.lowercase = kwargs.pop("lowercase", False)
|
||||
self.check_is_bool("lowercase")
|
||||
self.uppercase = kwargs.pop("uppercase", False)
|
||||
self.check_is_bool("uppercase")
|
||||
if self.uppercase and self.lowercase:
|
||||
raise ValueError(
|
||||
"the 'lowercase' and 'uppercase' arguments are mutually exclusive"
|
||||
)
|
||||
self.include_digits = kwargs.pop("include_digits", True)
|
||||
self.check_is_bool("include_digits")
|
||||
self.include_alpha = kwargs.pop("include_alpha", True)
|
||||
self.check_is_bool("include_alpha")
|
||||
self.include_punctuation = kwargs.pop("include_punctuation", False)
|
||||
self.keep_default = kwargs.pop("keep_default", False)
|
||||
self.check_is_bool("include_punctuation")
|
||||
self.max_unique_query_attempts = kwargs.pop(
|
||||
"max_unique_query_attempts", MAX_UNIQUE_QUERY_ATTEMPTS
|
||||
)
|
||||
|
||||
# Set unique=False unless it's been set manually.
|
||||
if "unique" not in kwargs:
|
||||
kwargs["unique"] = False
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def random_char_generator(self, chars):
|
||||
for i in range(self.max_unique_query_attempts):
|
||||
yield "".join(get_random_string(self.length, chars))
|
||||
raise RuntimeError(
|
||||
"max random character attempts exceeded (%s)"
|
||||
% self.max_unique_query_attempts
|
||||
)
|
||||
|
||||
def in_unique_together(self, model_instance):
|
||||
for params in model_instance._meta.unique_together:
|
||||
if self.attname in params:
|
||||
return True
|
||||
return False
|
||||
|
||||
def pre_save(self, model_instance, add):
|
||||
if (not add or self.keep_default) and getattr(
|
||||
model_instance, self.attname
|
||||
) != "":
|
||||
return getattr(model_instance, self.attname)
|
||||
|
||||
population = ""
|
||||
if self.include_alpha:
|
||||
if self.lowercase:
|
||||
population += string.ascii_lowercase
|
||||
elif self.uppercase:
|
||||
population += string.ascii_uppercase
|
||||
else:
|
||||
population += string.ascii_letters
|
||||
|
||||
if self.include_digits:
|
||||
population += string.digits
|
||||
|
||||
if self.include_punctuation:
|
||||
population += string.punctuation
|
||||
|
||||
random_chars = self.random_char_generator(population)
|
||||
if not self.unique and not self.in_unique_together(model_instance):
|
||||
new = next(random_chars)
|
||||
setattr(model_instance, self.attname, new)
|
||||
return new
|
||||
|
||||
return self.find_unique(
|
||||
model_instance,
|
||||
model_instance._meta.get_field(self.attname),
|
||||
random_chars,
|
||||
)
|
||||
|
||||
def internal_type(self):
|
||||
return "CharField"
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
kwargs["length"] = self.length
|
||||
del kwargs["max_length"]
|
||||
if self.lowercase is True:
|
||||
kwargs["lowercase"] = self.lowercase
|
||||
if self.uppercase is True:
|
||||
kwargs["uppercase"] = self.uppercase
|
||||
if self.include_alpha is False:
|
||||
kwargs["include_alpha"] = self.include_alpha
|
||||
if self.include_digits is False:
|
||||
kwargs["include_digits"] = self.include_digits
|
||||
if self.include_punctuation is True:
|
||||
kwargs["include_punctuation"] = self.include_punctuation
|
||||
if self.unique is True:
|
||||
kwargs["unique"] = self.unique
|
||||
return name, path, args, kwargs
|
||||
|
||||
|
||||
class CreationDateTimeField(DateTimeField):
|
||||
"""
|
||||
CreationDateTimeField
|
||||
|
||||
By default, sets editable=False, blank=True, auto_now_add=True
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("editable", False)
|
||||
kwargs.setdefault("blank", True)
|
||||
kwargs.setdefault("auto_now_add", True)
|
||||
DateTimeField.__init__(self, *args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "DateTimeField"
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
if self.editable is not False:
|
||||
kwargs["editable"] = True
|
||||
if self.blank is not True:
|
||||
kwargs["blank"] = False
|
||||
if self.auto_now_add is not False:
|
||||
kwargs["auto_now_add"] = True
|
||||
return name, path, args, kwargs
|
||||
|
||||
|
||||
class ModificationDateTimeField(CreationDateTimeField):
|
||||
"""
|
||||
ModificationDateTimeField
|
||||
|
||||
By default, sets editable=False, blank=True, auto_now=True
|
||||
|
||||
Sets value to now every time the object is saved.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("auto_now", True)
|
||||
DateTimeField.__init__(self, *args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "DateTimeField"
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
if self.auto_now is not False:
|
||||
kwargs["auto_now"] = True
|
||||
return name, path, args, kwargs
|
||||
|
||||
def pre_save(self, model_instance, add):
|
||||
if not getattr(model_instance, "update_modified", True):
|
||||
return getattr(model_instance, self.attname)
|
||||
return super().pre_save(model_instance, add)
|
||||
|
||||
|
||||
class UUIDVersionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UUIDFieldMixin:
|
||||
"""
|
||||
UUIDFieldMixin
|
||||
|
||||
By default uses UUID version 4 (randomly generated UUID).
|
||||
|
||||
The field support all uuid versions which are natively supported by the uuid python module, except version 2.
|
||||
For more information see: https://docs.python.org/lib/module-uuid.html
|
||||
""" # noqa: E501
|
||||
|
||||
DEFAULT_MAX_LENGTH = 36
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
verbose_name=None,
|
||||
name=None,
|
||||
auto=True,
|
||||
version=4,
|
||||
node=None,
|
||||
clock_seq=None,
|
||||
namespace=None,
|
||||
uuid_name=None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
if not HAS_UUID:
|
||||
raise ImproperlyConfigured(
|
||||
"'uuid' module is required for UUIDField. "
|
||||
"(Do you have Python 2.5 or higher installed ?)"
|
||||
)
|
||||
|
||||
kwargs.setdefault("max_length", self.DEFAULT_MAX_LENGTH)
|
||||
|
||||
if auto:
|
||||
self.empty_strings_allowed = False
|
||||
kwargs["blank"] = True
|
||||
kwargs.setdefault("editable", False)
|
||||
|
||||
self.auto = auto
|
||||
self.version = version
|
||||
self.node = node
|
||||
self.clock_seq = clock_seq
|
||||
self.namespace = namespace
|
||||
self.uuid_name = uuid_name or name
|
||||
|
||||
super().__init__(verbose_name=verbose_name, *args, **kwargs)
|
||||
|
||||
def create_uuid(self):
|
||||
if not self.version or self.version == 4:
|
||||
return uuid.uuid4()
|
||||
elif self.version == 1:
|
||||
return uuid.uuid1(self.node, self.clock_seq)
|
||||
elif self.version == 2:
|
||||
raise UUIDVersionError("UUID version 2 is not supported.")
|
||||
elif self.version == 3:
|
||||
return uuid.uuid3(self.namespace, self.uuid_name)
|
||||
elif self.version == 5:
|
||||
return uuid.uuid5(self.namespace, self.uuid_name)
|
||||
else:
|
||||
raise UUIDVersionError("UUID version %s is not valid." % self.version)
|
||||
|
||||
def pre_save(self, model_instance, add):
|
||||
value = super().pre_save(model_instance, add)
|
||||
|
||||
if self.auto and add and value is None:
|
||||
value = force_str(self.create_uuid())
|
||||
setattr(model_instance, self.attname, value)
|
||||
return value
|
||||
else:
|
||||
if self.auto and not value:
|
||||
value = force_str(self.create_uuid())
|
||||
setattr(model_instance, self.attname, value)
|
||||
|
||||
return value
|
||||
|
||||
def formfield(self, form_class=None, choices_form_class=None, **kwargs):
|
||||
if self.auto:
|
||||
return None
|
||||
return super().formfield(form_class, choices_form_class, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
|
||||
if kwargs.get("max_length", None) == self.DEFAULT_MAX_LENGTH:
|
||||
del kwargs["max_length"]
|
||||
if self.auto is not True:
|
||||
kwargs["auto"] = self.auto
|
||||
if self.version != 4:
|
||||
kwargs["version"] = self.version
|
||||
if self.node is not None:
|
||||
kwargs["node"] = self.node
|
||||
if self.clock_seq is not None:
|
||||
kwargs["clock_seq"] = self.clock_seq
|
||||
if self.namespace is not None:
|
||||
kwargs["namespace"] = self.namespace
|
||||
if self.uuid_name is not None:
|
||||
kwargs["uuid_name"] = self.name
|
||||
|
||||
return name, path, args, kwargs
|
||||
|
||||
|
||||
class ShortUUIDField(UUIDFieldMixin, CharField):
|
||||
"""
|
||||
ShortUUIDFied
|
||||
|
||||
Generates concise (22 characters instead of 36), unambiguous, URL-safe UUIDs.
|
||||
|
||||
Based on `shortuuid`: https://github.com/stochastic-technologies/shortuuid
|
||||
"""
|
||||
|
||||
DEFAULT_MAX_LENGTH = 22
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not HAS_SHORT_UUID:
|
||||
raise ImproperlyConfigured(
|
||||
"'shortuuid' module is required for ShortUUIDField. "
|
||||
"(Do you have Python 2.5 or higher installed ?)"
|
||||
)
|
||||
kwargs.setdefault("max_length", self.DEFAULT_MAX_LENGTH)
|
||||
|
||||
def create_uuid(self):
|
||||
if not self.version or self.version == 4:
|
||||
return shortuuid.uuid()
|
||||
elif self.version == 1:
|
||||
return shortuuid.uuid()
|
||||
elif self.version == 2:
|
||||
raise UUIDVersionError("UUID version 2 is not supported.")
|
||||
elif self.version == 3:
|
||||
raise UUIDVersionError("UUID version 3 is not supported.")
|
||||
elif self.version == 5:
|
||||
return shortuuid.uuid(name=self.namespace)
|
||||
else:
|
||||
raise UUIDVersionError("UUID version %s is not valid." % self.version)
|
||||
@@ -0,0 +1,115 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JSONField automatically serializes most Python terms to JSON data.
|
||||
Creates a TEXT field with a default value of "{}". See test_json.py for
|
||||
more information.
|
||||
|
||||
from django.db import models
|
||||
from django_extensions.db.fields import json
|
||||
|
||||
class LOL(models.Model):
|
||||
extra = json.JSONField()
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db import models
|
||||
from django.db.models import expressions
|
||||
|
||||
|
||||
def dumps(value):
|
||||
return DjangoJSONEncoder().encode(value)
|
||||
|
||||
|
||||
def loads(txt):
|
||||
return json.loads(txt)
|
||||
|
||||
|
||||
class JSONDict(dict):
|
||||
"""
|
||||
Hack so repr() called by dumpdata will output JSON instead of
|
||||
Python formatted data. This way fixtures will work!
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
return dumps(self)
|
||||
|
||||
|
||||
class JSONList(list):
|
||||
"""
|
||||
Hack so repr() called by dumpdata will output JSON instead of
|
||||
Python formatted data. This way fixtures will work!
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
return dumps(self)
|
||||
|
||||
|
||||
class JSONField(models.TextField):
|
||||
"""
|
||||
JSONField is a generic textfield that neatly serializes/unserializes
|
||||
JSON objects seamlessly. Main thingy must be a dict object.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["default"] = kwargs.get("default", dict)
|
||||
models.TextField.__init__(self, *args, **kwargs)
|
||||
|
||||
def get_default(self):
|
||||
if self.has_default():
|
||||
default = self.default
|
||||
|
||||
if callable(default):
|
||||
default = default()
|
||||
|
||||
return self.to_python(default)
|
||||
return super().get_default()
|
||||
|
||||
def to_python(self, value):
|
||||
"""Convert our string value to JSON after we load it from the DB"""
|
||||
if value is None or value == "":
|
||||
return {}
|
||||
|
||||
if isinstance(value, str):
|
||||
res = loads(value)
|
||||
else:
|
||||
res = value
|
||||
|
||||
if isinstance(res, dict):
|
||||
return JSONDict(**res)
|
||||
elif isinstance(res, list):
|
||||
return JSONList(res)
|
||||
|
||||
return res
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if not isinstance(value, str):
|
||||
return dumps(value)
|
||||
return super(models.TextField, self).get_prep_value(value)
|
||||
|
||||
def from_db_value(self, value, expression, connection): # type: ignore
|
||||
return self.to_python(value)
|
||||
|
||||
def get_db_prep_save(self, value, connection, **kwargs):
|
||||
"""Convert our JSON object to a string before we save"""
|
||||
if value is None and self.null:
|
||||
return None
|
||||
|
||||
# default values come in as strings; only non-strings should be
|
||||
# run through `dumps`
|
||||
if (
|
||||
not isinstance(value, str)
|
||||
# https://github.com/django-extensions/django-extensions/issues/1924
|
||||
# https://code.djangoproject.com/ticket/35167
|
||||
and not isinstance(value, expressions.Expression)
|
||||
):
|
||||
value = dumps(value)
|
||||
|
||||
return super().get_db_prep_save(value, connection)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
if self.default == "{}":
|
||||
del kwargs["default"]
|
||||
return name, path, args, kwargs
|
||||
@@ -0,0 +1,150 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.db import models
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_extensions.db.fields import (
|
||||
AutoSlugField,
|
||||
CreationDateTimeField,
|
||||
ModificationDateTimeField,
|
||||
)
|
||||
|
||||
|
||||
class TimeStampedModel(models.Model):
|
||||
"""
|
||||
TimeStampedModel
|
||||
|
||||
An abstract base class model that provides self-managed "created" and
|
||||
"modified" fields.
|
||||
"""
|
||||
|
||||
created = CreationDateTimeField(_("created"))
|
||||
modified = ModificationDateTimeField(_("modified"))
|
||||
|
||||
def save(self, **kwargs):
|
||||
self.update_modified = kwargs.pop(
|
||||
"update_modified", getattr(self, "update_modified", True)
|
||||
)
|
||||
super().save(**kwargs)
|
||||
|
||||
class Meta:
|
||||
get_latest_by = "modified"
|
||||
abstract = True
|
||||
|
||||
|
||||
class TitleDescriptionModel(models.Model):
|
||||
"""
|
||||
TitleDescriptionModel
|
||||
|
||||
An abstract base class model that provides title and description fields.
|
||||
"""
|
||||
|
||||
title = models.CharField(_("title"), max_length=255)
|
||||
description = models.TextField(_("description"), blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class TitleSlugDescriptionModel(TitleDescriptionModel):
|
||||
"""
|
||||
TitleSlugDescriptionModel
|
||||
|
||||
An abstract base class model that provides title and description fields
|
||||
and a self-managed "slug" field that populates from the title.
|
||||
|
||||
.. note ::
|
||||
If you want to use custom "slugify" function, you could
|
||||
define ``slugify_function`` which then will be used
|
||||
in :py:class:`AutoSlugField` to slugify ``populate_from`` field.
|
||||
|
||||
See :py:class:`AutoSlugField` for more details.
|
||||
"""
|
||||
|
||||
slug = AutoSlugField(_("slug"), populate_from="title")
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class ActivatorQuerySet(models.query.QuerySet):
|
||||
"""
|
||||
ActivatorQuerySet
|
||||
|
||||
Query set that returns statused results
|
||||
"""
|
||||
|
||||
def active(self):
|
||||
"""Return active query set"""
|
||||
return self.filter(status=ActivatorModel.ACTIVE_STATUS)
|
||||
|
||||
def inactive(self):
|
||||
"""Return inactive query set"""
|
||||
return self.filter(status=ActivatorModel.INACTIVE_STATUS)
|
||||
|
||||
|
||||
class ActivatorModelManager(models.Manager):
|
||||
"""
|
||||
ActivatorModelManager
|
||||
|
||||
Manager to return instances of ActivatorModel:
|
||||
SomeModel.objects.active() / .inactive()
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
"""Use ActivatorQuerySet for all results"""
|
||||
return ActivatorQuerySet(model=self.model, using=self._db)
|
||||
|
||||
def active(self):
|
||||
"""
|
||||
Return active instances of ActivatorModel:
|
||||
|
||||
SomeModel.objects.active(), proxy to ActivatorQuerySet.active
|
||||
"""
|
||||
return self.get_queryset().active()
|
||||
|
||||
def inactive(self):
|
||||
"""
|
||||
Return inactive instances of ActivatorModel:
|
||||
|
||||
SomeModel.objects.inactive(), proxy to ActivatorQuerySet.inactive
|
||||
"""
|
||||
return self.get_queryset().inactive()
|
||||
|
||||
|
||||
class ActivatorModel(models.Model):
|
||||
"""
|
||||
ActivatorModel
|
||||
|
||||
An abstract base class model that provides activate and deactivate fields.
|
||||
"""
|
||||
|
||||
INACTIVE_STATUS = 0
|
||||
ACTIVE_STATUS = 1
|
||||
|
||||
STATUS_CHOICES = (
|
||||
(INACTIVE_STATUS, _("Inactive")),
|
||||
(ACTIVE_STATUS, _("Active")),
|
||||
)
|
||||
status = models.IntegerField(
|
||||
_("status"), choices=STATUS_CHOICES, default=ACTIVE_STATUS
|
||||
)
|
||||
activate_date = models.DateTimeField(
|
||||
blank=True, null=True, help_text=_("keep empty for an immediate activation")
|
||||
)
|
||||
deactivate_date = models.DateTimeField(
|
||||
blank=True, null=True, help_text=_("keep empty for indefinite activation")
|
||||
)
|
||||
objects = ActivatorModelManager()
|
||||
|
||||
class Meta:
|
||||
ordering = (
|
||||
"status",
|
||||
"-activate_date",
|
||||
)
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.activate_date:
|
||||
self.activate_date = now()
|
||||
super().save(*args, **kwargs)
|
||||
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from importlib import import_module
|
||||
from inspect import (
|
||||
getmembers,
|
||||
isclass,
|
||||
)
|
||||
from pkgutil import walk_packages
|
||||
from typing import ( # NOQA
|
||||
Dict,
|
||||
List,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
|
||||
class SubclassesFinder:
|
||||
def __init__(self, base_classes_from_settings):
|
||||
self.base_classes = []
|
||||
for element in base_classes_from_settings:
|
||||
if isinstance(element, str):
|
||||
element = import_string(element)
|
||||
self.base_classes.append(element)
|
||||
|
||||
def _should_be_imported(self, candidate_to_import): # type: (Tuple[str, type]) -> bool
|
||||
for base_class in self.base_classes:
|
||||
if issubclass(candidate_to_import[1], base_class):
|
||||
return True
|
||||
return False
|
||||
|
||||
def collect_subclasses(self): # type: () -> Dict[str, List[Tuple[str, str]]]
|
||||
"""
|
||||
Collect all subclasses of user-defined base classes from project.
|
||||
:return: Dictionary from module name to list of tuples.
|
||||
First element of tuple is model name and second is alias.
|
||||
Currently we set alias equal to model name,
|
||||
but in future functionality of aliasing subclasses can be added.
|
||||
"""
|
||||
result = {} # type: Dict[str, List[Tuple[str, str]]]
|
||||
for loader, module_name, is_pkg in walk_packages(path=[str(settings.BASE_DIR)]):
|
||||
subclasses_from_module = self._collect_classes_from_module(module_name)
|
||||
if subclasses_from_module:
|
||||
result[module_name] = subclasses_from_module
|
||||
return result
|
||||
|
||||
def _collect_classes_from_module(self, module_name): # type: (str) -> List[Tuple[str, str]]
|
||||
for excluded_module in getattr(
|
||||
settings, "SHELL_PLUS_SUBCLASSES_IMPORT_MODULES_BLACKLIST", []
|
||||
):
|
||||
if module_name.startswith(excluded_module):
|
||||
return []
|
||||
imported_module = import_module(module_name)
|
||||
classes_to_import = getmembers(
|
||||
imported_module,
|
||||
lambda element: isclass(element)
|
||||
and element.__module__ == imported_module.__name__,
|
||||
)
|
||||
classes_to_import = list(filter(self._should_be_imported, classes_to_import))
|
||||
return [(name, name) for name, _ in classes_to_import]
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Daily cleanup job.
|
||||
|
||||
Can be run as a cronjob to clean out old data from the database (only expired
|
||||
sessions at the moment).
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import caches
|
||||
|
||||
from django_extensions.management.jobs import DailyJob
|
||||
|
||||
|
||||
class Job(DailyJob):
|
||||
help = "Cache (db) cleanup Job"
|
||||
|
||||
def execute(self):
|
||||
if hasattr(settings, "CACHES"):
|
||||
for cache_name, cache_options in settings.CACHES.items():
|
||||
if cache_options["BACKEND"].endswith("DatabaseCache"):
|
||||
cache = caches[cache_name]
|
||||
cache.clear()
|
||||
return
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Daily cleanup job.
|
||||
|
||||
Can be run as a cronjob to clean out old data from the database (only expired
|
||||
sessions at the moment).
|
||||
"""
|
||||
|
||||
from django_extensions.management.jobs import DailyJob
|
||||
|
||||
|
||||
class Job(DailyJob):
|
||||
help = "Django Daily Cleanup Job"
|
||||
|
||||
def execute(self):
|
||||
from django.core import management
|
||||
|
||||
management.call_command("clearsessions")
|
||||
@@ -0,0 +1,109 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-06-06 11:44+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: admin/__init__.py:139
|
||||
msgid "and"
|
||||
msgstr "و"
|
||||
|
||||
#: admin/__init__.py:141
|
||||
#, python-format
|
||||
msgid "Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr "إستعمل الحقل الأيسر من %(model_name)s لبحث ضمن الأحقال التالية %(field_list)s "
|
||||
|
||||
#: admin/filter.py:24 admin/filter.py:53
|
||||
msgid "Yes"
|
||||
msgstr "نعم"
|
||||
|
||||
#: admin/filter.py:25 admin/filter.py:54
|
||||
msgid "No"
|
||||
msgstr "لا"
|
||||
|
||||
#: admin/filter.py:32
|
||||
msgid "All"
|
||||
msgstr "كل"
|
||||
|
||||
#: db/models.py:18
|
||||
msgid "created"
|
||||
msgstr "تم تكونه"
|
||||
|
||||
#: db/models.py:19
|
||||
msgid "modified"
|
||||
msgstr "تم تعديله"
|
||||
|
||||
#: db/models.py:37
|
||||
msgid "title"
|
||||
msgstr "عنوان"
|
||||
|
||||
#: db/models.py:38
|
||||
msgid "description"
|
||||
msgstr "وصف"
|
||||
|
||||
#: db/models.py:59
|
||||
msgid "slug"
|
||||
msgstr "رابط "
|
||||
|
||||
#: db/models.py:120 mongodb/models.py:76
|
||||
msgid "Inactive"
|
||||
msgstr "غير نشط"
|
||||
|
||||
#: db/models.py:121 mongodb/models.py:77
|
||||
msgid "Active"
|
||||
msgstr "نشط"
|
||||
|
||||
#: db/models.py:123
|
||||
msgid "status"
|
||||
msgstr "الحالة"
|
||||
|
||||
#: db/models.py:124 mongodb/models.py:80
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr "أترك الحقل فارغ ليتم التنشيط مباشرة"
|
||||
|
||||
#: db/models.py:125 mongodb/models.py:81
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr "أترك الحقل فارغ لتنشيط لمدة غير محددة"
|
||||
|
||||
#: mongodb/fields/__init__.py:22
|
||||
#, python-format
|
||||
msgid "String (up to %(max_length)s)"
|
||||
msgstr "سلسلة الإحرف (طولها يصل إلى %(max_length)s)"
|
||||
|
||||
#: validators.py:14
|
||||
msgid "Control Characters like new lines or tabs are not allowed."
|
||||
msgstr "لا يسمح إستعمال أحرف تحكم مثل حرف العودة إلى السطر أو علامات التبويب"
|
||||
|
||||
#: validators.py:48
|
||||
msgid "Leading and Trailing whitespaces are not allowed."
|
||||
msgstr "المسافات البيضاء الزائدة عند البداية أو نهاية غير مسموح بها"
|
||||
|
||||
#: validators.py:74
|
||||
msgid "Only a hex string is allowed."
|
||||
msgstr "مسموح إستعمال سلسلة أحرف hex فقط"
|
||||
|
||||
#: validators.py:75
|
||||
#, python-format
|
||||
msgid "Invalid length. Must be %(length)d characters."
|
||||
msgstr "الطول غير مقبول, يجب أن لا يكون أطول من %(length)d"
|
||||
|
||||
#: validators.py:76
|
||||
#, python-format
|
||||
msgid "Ensure that there are more than %(min)s characters."
|
||||
msgstr "تأكد أن طول سلسلة الإحرف أطول من %(min)s "
|
||||
|
||||
#: validators.py:77
|
||||
#, python-format
|
||||
msgid "Ensure that there are no more than %(max)s characters."
|
||||
msgstr "تأكد أن طول سلسلة الأحرف لا تتجوز %(max)s "
|
||||
Binary file not shown.
@@ -0,0 +1,79 @@
|
||||
# django_extentions in Danish.
|
||||
# django_extensions på Dansk.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# Michael Lind Mortensen <illio@cs.au.dk>, 2009.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2011-02-02 11:42+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: admin/__init__.py:121
|
||||
msgid "and"
|
||||
msgstr "og"
|
||||
|
||||
#: admin/__init__.py:123
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr ""
|
||||
"Brug feltet til venstre til at lave %(model_name)s lookups i felterne %"
|
||||
"(field_list)s."
|
||||
|
||||
#: db/models.py:15
|
||||
msgid "created"
|
||||
msgstr "skabt"
|
||||
|
||||
#: db/models.py:16
|
||||
msgid "modified"
|
||||
msgstr "ændret"
|
||||
|
||||
#: db/models.py:26
|
||||
msgid "title"
|
||||
msgstr "titel"
|
||||
|
||||
#: db/models.py:27
|
||||
msgid "slug"
|
||||
msgstr "slug"
|
||||
|
||||
#: db/models.py:28
|
||||
msgid "description"
|
||||
msgstr "beskrivelse"
|
||||
|
||||
#: db/models.py:50
|
||||
msgid "Inactive"
|
||||
msgstr ""
|
||||
|
||||
#: db/models.py:51
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
|
||||
#: db/models.py:53
|
||||
msgid "status"
|
||||
msgstr ""
|
||||
|
||||
#: db/models.py:56
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr ""
|
||||
|
||||
#: db/models.py:58
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr ""
|
||||
|
||||
#: management/commands/show_urls.py:34
|
||||
#, python-format
|
||||
msgid "%s does not appear to be a urlpattern object"
|
||||
msgstr ""
|
||||
|
||||
#: templates/django_extensions/widgets/foreignkey_searchinput.html:4
|
||||
msgid "Lookup"
|
||||
msgstr "Lookup"
|
||||
Binary file not shown.
@@ -0,0 +1,77 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2011-02-02 11:42+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: admin/__init__.py:121
|
||||
msgid "and"
|
||||
msgstr "und"
|
||||
|
||||
#: admin/__init__.py:123
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr ""
|
||||
"Das linke Feld benutzen, um %(model_name)s Abfragen in den Feldern %"
|
||||
"(field_list)s durchführen."
|
||||
|
||||
#: db/models.py:15
|
||||
msgid "created"
|
||||
msgstr "erstellt"
|
||||
|
||||
#: db/models.py:16
|
||||
msgid "modified"
|
||||
msgstr "geändert"
|
||||
|
||||
#: db/models.py:26
|
||||
msgid "title"
|
||||
msgstr "Titel"
|
||||
|
||||
#: db/models.py:27
|
||||
msgid "slug"
|
||||
msgstr "Slug"
|
||||
|
||||
#: db/models.py:28
|
||||
msgid "description"
|
||||
msgstr "Beschreibung"
|
||||
|
||||
#: db/models.py:50
|
||||
msgid "Inactive"
|
||||
msgstr "Inaktiv"
|
||||
|
||||
#: db/models.py:51
|
||||
msgid "Active"
|
||||
msgstr "Aktiv"
|
||||
|
||||
#: db/models.py:53
|
||||
msgid "status"
|
||||
msgstr "Status"
|
||||
|
||||
#: db/models.py:56
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr "Leer lassen für sofortige Aktivierung"
|
||||
|
||||
#: db/models.py:58
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr "Leer lassen für unbefristete Aktivierung"
|
||||
|
||||
#: management/commands/show_urls.py:34
|
||||
#, python-format
|
||||
msgid "%s does not appear to be a urlpattern object"
|
||||
msgstr "%s ist kein urlpattern Objekt"
|
||||
|
||||
#: templates/django_extensions/widgets/foreignkey_searchinput.html:4
|
||||
msgid "Lookup"
|
||||
msgstr "Abfrage"
|
||||
Binary file not shown.
@@ -0,0 +1,79 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-extensions\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2011-02-02 11:42+0100\n"
|
||||
"PO-Revision-Date: 2011-02-02 10:38+0000\n"
|
||||
"Last-Translator: Jannis <jannis@leidel.info>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: el\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
||||
|
||||
#: admin/__init__.py:121
|
||||
msgid "and"
|
||||
msgstr "και"
|
||||
|
||||
#: admin/__init__.py:123
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr ""
|
||||
"Χρησιμοποίησε το αριστερό πεδίο για να κάνεις αναζήτηση του %(model_name)s "
|
||||
"με βάσει τα πεδία %(field_list)s."
|
||||
|
||||
#: db/models.py:15
|
||||
msgid "created"
|
||||
msgstr "δημιουργήθηκε"
|
||||
|
||||
#: db/models.py:16
|
||||
msgid "modified"
|
||||
msgstr "τροποποιήθηκε"
|
||||
|
||||
#: db/models.py:26
|
||||
msgid "title"
|
||||
msgstr "τίτλος"
|
||||
|
||||
#: db/models.py:27
|
||||
msgid "slug"
|
||||
msgstr "μίνι-όνομα"
|
||||
|
||||
#: db/models.py:28
|
||||
msgid "description"
|
||||
msgstr "περιγραφή"
|
||||
|
||||
#: db/models.py:50
|
||||
msgid "Inactive"
|
||||
msgstr "ανενεργό"
|
||||
|
||||
#: db/models.py:51
|
||||
msgid "Active"
|
||||
msgstr "Ενεργό"
|
||||
|
||||
#: db/models.py:53
|
||||
msgid "status"
|
||||
msgstr "κατάσταση"
|
||||
|
||||
#: db/models.py:56
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr "αφήστε άδειο για άμεση ενεργοποίηση"
|
||||
|
||||
#: db/models.py:58
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr "αφήστε άδειο για αόριστη ενεργοποίηση"
|
||||
|
||||
#: management/commands/show_urls.py:34
|
||||
#, python-format
|
||||
msgid "%s does not appear to be a urlpattern object"
|
||||
msgstr "%s δεν φαίνεται να είναι ένα αντικείμενο urlpattern"
|
||||
|
||||
#: templates/django_extensions/widgets/foreignkey_searchinput.html:4
|
||||
msgid "Lookup"
|
||||
msgstr "Αναζήτηση"
|
||||
Binary file not shown.
@@ -0,0 +1,112 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-02-10 20:37+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: en\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: admin/__init__.py:142
|
||||
msgid "and"
|
||||
msgstr ""
|
||||
|
||||
#: admin/__init__.py:144
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr ""
|
||||
|
||||
#: admin/filter.py:24 admin/filter.py:53
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: admin/filter.py:25 admin/filter.py:54
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
#: admin/filter.py:32
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
#: db/models.py:18
|
||||
msgid "created"
|
||||
msgstr ""
|
||||
|
||||
#: db/models.py:19
|
||||
msgid "modified"
|
||||
msgstr ""
|
||||
|
||||
#: db/models.py:38
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
#: db/models.py:39
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: db/models.py:60
|
||||
msgid "slug"
|
||||
msgstr ""
|
||||
|
||||
#: db/models.py:121 mongodb/models.py:76
|
||||
msgid "Inactive"
|
||||
msgstr ""
|
||||
|
||||
#: db/models.py:122 mongodb/models.py:77
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
|
||||
#: db/models.py:124
|
||||
msgid "status"
|
||||
msgstr ""
|
||||
|
||||
#: db/models.py:125 mongodb/models.py:80
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr ""
|
||||
|
||||
#: db/models.py:126 mongodb/models.py:81
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr ""
|
||||
|
||||
#: mongodb/fields/__init__.py:22
|
||||
#, python-format
|
||||
msgid "String (up to %(max_length)s)"
|
||||
msgstr ""
|
||||
|
||||
#: validators.py:14
|
||||
msgid "Control Characters like new lines or tabs are not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: validators.py:48
|
||||
msgid "Leading and Trailing whitespaces are not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: validators.py:74
|
||||
msgid "Only a hex string is allowed."
|
||||
msgstr ""
|
||||
|
||||
#: validators.py:75
|
||||
#, python-format
|
||||
msgid "Invalid length. Must be %(length)d characters."
|
||||
msgstr ""
|
||||
|
||||
#: validators.py:76
|
||||
#, python-format
|
||||
msgid "Ensure that there are more than %(min)s characters."
|
||||
msgstr ""
|
||||
|
||||
#: validators.py:77
|
||||
#, python-format
|
||||
msgid "Ensure that there are no more than %(max)s characters."
|
||||
msgstr ""
|
||||
Binary file not shown.
@@ -0,0 +1,77 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2011-02-02 11:43+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: admin/__init__.py:121
|
||||
msgid "and"
|
||||
msgstr "y"
|
||||
|
||||
#: admin/__init__.py:123
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr ""
|
||||
"Utilice el campo de la izquierda para hacer búsquedas en los campos %"
|
||||
"(field_list)s de %(model_name)s."
|
||||
|
||||
#: db/models.py:15
|
||||
msgid "created"
|
||||
msgstr "creado"
|
||||
|
||||
#: db/models.py:16
|
||||
msgid "modified"
|
||||
msgstr "modificado"
|
||||
|
||||
#: db/models.py:26
|
||||
msgid "title"
|
||||
msgstr "titulo"
|
||||
|
||||
#: db/models.py:27
|
||||
msgid "slug"
|
||||
msgstr "slug"
|
||||
|
||||
#: db/models.py:28
|
||||
msgid "description"
|
||||
msgstr "descripción"
|
||||
|
||||
#: db/models.py:50
|
||||
msgid "Inactive"
|
||||
msgstr "Inactivo"
|
||||
|
||||
#: db/models.py:51
|
||||
msgid "Active"
|
||||
msgstr "Activo"
|
||||
|
||||
#: db/models.py:53
|
||||
msgid "status"
|
||||
msgstr "estado"
|
||||
|
||||
#: db/models.py:56
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr "mantener vacío para una activación inmediata"
|
||||
|
||||
#: db/models.py:58
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr "deje vacío para mantener la activación indefinida"
|
||||
|
||||
#: management/commands/show_urls.py:34
|
||||
#, python-format
|
||||
msgid "%s does not appear to be a urlpattern object"
|
||||
msgstr "% s no parece ser un objeto urlpattern"
|
||||
|
||||
#: templates/django_extensions/widgets/foreignkey_searchinput.html:4
|
||||
msgid "Lookup"
|
||||
msgstr "Buscar"
|
||||
Binary file not shown.
@@ -0,0 +1,81 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# mathiasuk, 2014
|
||||
# mathiasuk, 2014
|
||||
# stevandoh <stevandoh@gmail.com>, 2013
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-extensions\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2011-02-02 11:42+0100\n"
|
||||
"PO-Revision-Date: 2014-01-11 11:14+0000\n"
|
||||
"Last-Translator: mathiasuk\n"
|
||||
"Language-Team: French (https://www.transifex.com/projects/p/django-extensions/language/fr/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: fr\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: admin/__init__.py:121
|
||||
msgid "and"
|
||||
msgstr "et"
|
||||
|
||||
#: admin/__init__.py:123
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields "
|
||||
"%(field_list)s."
|
||||
msgstr "Utilisez le champ de gauche pour faire des recheres de %(model_name)s dans les champs %(field_list)s."
|
||||
|
||||
#: db/models.py:15
|
||||
msgid "created"
|
||||
msgstr "créé"
|
||||
|
||||
#: db/models.py:16
|
||||
msgid "modified"
|
||||
msgstr "mis à jour"
|
||||
|
||||
#: db/models.py:26
|
||||
msgid "title"
|
||||
msgstr "titre"
|
||||
|
||||
#: db/models.py:27
|
||||
msgid "slug"
|
||||
msgstr "slug"
|
||||
|
||||
#: db/models.py:28
|
||||
msgid "description"
|
||||
msgstr "description"
|
||||
|
||||
#: db/models.py:50
|
||||
msgid "Inactive"
|
||||
msgstr "Inactif"
|
||||
|
||||
#: db/models.py:51
|
||||
msgid "Active"
|
||||
msgstr "Actif"
|
||||
|
||||
#: db/models.py:53
|
||||
msgid "status"
|
||||
msgstr "état"
|
||||
|
||||
#: db/models.py:56
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr "laisser vide pour activation immédiate"
|
||||
|
||||
#: db/models.py:58
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr "laisser vide pour activation indéterminée"
|
||||
|
||||
#: management/commands/show_urls.py:34
|
||||
#, python-format
|
||||
msgid "%s does not appear to be a urlpattern object"
|
||||
msgstr "%s ne semble pas etre un object urlpattern"
|
||||
|
||||
#: templates/django_extensions/widgets/foreignkey_searchinput.html:4
|
||||
msgid "Lookup"
|
||||
msgstr "Recherche"
|
||||
Binary file not shown.
@@ -0,0 +1,77 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2011-02-02 11:43+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: admin/__init__.py:121
|
||||
msgid "and"
|
||||
msgstr "és"
|
||||
|
||||
#: admin/__init__.py:123
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr ""
|
||||
"Használd a baloldali mezőt hogy keress a %(model_name)s %(field_list)s. "
|
||||
"mezőiben"
|
||||
|
||||
#: db/models.py:15
|
||||
msgid "created"
|
||||
msgstr "létrehozva"
|
||||
|
||||
#: db/models.py:16
|
||||
msgid "modified"
|
||||
msgstr "módosítva"
|
||||
|
||||
#: db/models.py:26
|
||||
msgid "title"
|
||||
msgstr "Cím"
|
||||
|
||||
#: db/models.py:27
|
||||
msgid "slug"
|
||||
msgstr "Slug"
|
||||
|
||||
#: db/models.py:28
|
||||
msgid "description"
|
||||
msgstr "Leírás"
|
||||
|
||||
#: db/models.py:50
|
||||
msgid "Inactive"
|
||||
msgstr "Inaktív"
|
||||
|
||||
#: db/models.py:51
|
||||
msgid "Active"
|
||||
msgstr "Aktív"
|
||||
|
||||
#: db/models.py:53
|
||||
msgid "status"
|
||||
msgstr "Állapot"
|
||||
|
||||
#: db/models.py:56
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr "Üresen hagyni azonnali aktiváláshoz"
|
||||
|
||||
#: db/models.py:58
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr "Üresen hagyni korlátlan aktiváláshoz"
|
||||
|
||||
#: management/commands/show_urls.py:34
|
||||
#, python-format
|
||||
msgid "%s does not appear to be a urlpattern object"
|
||||
msgstr "Úgy néz ki hogy %s nem egy urlpattern objektum"
|
||||
|
||||
#: templates/django_extensions/widgets/foreignkey_searchinput.html:4
|
||||
msgid "Lookup"
|
||||
msgstr "Lekérdezés"
|
||||
Binary file not shown.
@@ -0,0 +1,98 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-extensions\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-07-27 22:25+0700\n"
|
||||
"PO-Revision-Date: 2020-07-28 10:48+0700\n"
|
||||
"Last-Translator: Sutrisno Efendi <kangfend@gmail.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n\n"
|
||||
"Language: id\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: admin/__init__.py:139
|
||||
msgid "and"
|
||||
msgstr "dan"
|
||||
|
||||
#: admin/__init__.py:141
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr ""
|
||||
"Gunakan bidang sebelah kiri untuk pencarian %(model_name)s pada bidang %(field_list)s."
|
||||
|
||||
#: admin/filter.py:24 admin/filter.py:53
|
||||
msgid "Yes"
|
||||
msgstr "Ya"
|
||||
|
||||
#: admin/filter.py:25 admin/filter.py:54
|
||||
msgid "No"
|
||||
msgstr "Tidak"
|
||||
|
||||
#: admin/filter.py:32
|
||||
msgid "All"
|
||||
msgstr "Semua"
|
||||
|
||||
#: db/models.py:18
|
||||
msgid "created"
|
||||
msgstr "dibuat"
|
||||
|
||||
#: db/models.py:19
|
||||
msgid "modified"
|
||||
msgstr "diubah"
|
||||
|
||||
#: db/models.py:37
|
||||
msgid "title"
|
||||
msgstr "judul"
|
||||
|
||||
#: db/models.py:38
|
||||
msgid "description"
|
||||
msgstr "deskripsi"
|
||||
|
||||
#: db/models.py:59
|
||||
msgid "slug"
|
||||
msgstr "slug"
|
||||
|
||||
#: db/models.py:120 mongodb/models.py:76
|
||||
msgid "Inactive"
|
||||
msgstr "Nonaktif"
|
||||
|
||||
#: db/models.py:121 mongodb/models.py:77
|
||||
msgid "Active"
|
||||
msgstr "Aktif"
|
||||
|
||||
#: db/models.py:123
|
||||
msgid "status"
|
||||
msgstr "status"
|
||||
|
||||
#: mongodb/fields/__init__.py:22
|
||||
#, python-format
|
||||
msgid "String (up to %(max_length)s)"
|
||||
msgstr "String (hingga %(max_length)s)"
|
||||
|
||||
#: validators.py:74
|
||||
msgid "Only a hex string is allowed."
|
||||
msgstr "Hanya string hex yang diizinkan."
|
||||
|
||||
#: validators.py:75
|
||||
#, python-format
|
||||
msgid "Invalid length. Must be %(length)d characters."
|
||||
msgstr "Panjang tidak valid. Harus %(length)d karakter."
|
||||
|
||||
#: validators.py:76
|
||||
#, python-format
|
||||
msgid "Ensure that there are more than %(min)s characters."
|
||||
msgstr "Pastikan lebih dari %(min)s karakter."
|
||||
|
||||
#: validators.py:77
|
||||
#, python-format
|
||||
msgid "Ensure that there are no more than %(max)s characters."
|
||||
msgstr "Pastikan tidak lebih dari %(max)s karakter."
|
||||
Binary file not shown.
@@ -0,0 +1,77 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2011-02-02 11:43+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: admin/__init__.py:121
|
||||
msgid "and"
|
||||
msgstr "e"
|
||||
|
||||
#: admin/__init__.py:123
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr ""
|
||||
"Utilizzare il campo a sinistra per fare ricerche nei campi %(field_list)s "
|
||||
"del modello %(model_name)s."
|
||||
|
||||
#: db/models.py:15
|
||||
msgid "created"
|
||||
msgstr "creato"
|
||||
|
||||
#: db/models.py:16
|
||||
msgid "modified"
|
||||
msgstr "modificato"
|
||||
|
||||
#: db/models.py:26
|
||||
msgid "title"
|
||||
msgstr "titolo"
|
||||
|
||||
#: db/models.py:27
|
||||
msgid "slug"
|
||||
msgstr "slug"
|
||||
|
||||
#: db/models.py:28
|
||||
msgid "description"
|
||||
msgstr "descrizione"
|
||||
|
||||
#: db/models.py:50
|
||||
msgid "Inactive"
|
||||
msgstr "Inattivo"
|
||||
|
||||
#: db/models.py:51
|
||||
msgid "Active"
|
||||
msgstr "Attivo"
|
||||
|
||||
#: db/models.py:53
|
||||
msgid "status"
|
||||
msgstr "stato"
|
||||
|
||||
#: db/models.py:56
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr "lasciare vuoto per attivazione immediata"
|
||||
|
||||
#: db/models.py:58
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr "lasciare vuoti per attivazione indefinita"
|
||||
|
||||
#: management/commands/show_urls.py:34
|
||||
#, python-format
|
||||
msgid "%s does not appear to be a urlpattern object"
|
||||
msgstr "% s non sembra essere un oggetto urlPattern"
|
||||
|
||||
#: templates/django_extensions/widgets/foreignkey_searchinput.html:4
|
||||
msgid "Lookup"
|
||||
msgstr "Ricerca"
|
||||
Binary file not shown.
@@ -0,0 +1,77 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2011-02-02 11:43+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: admin/__init__.py:121
|
||||
msgid "and"
|
||||
msgstr "と"
|
||||
|
||||
#: admin/__init__.py:123
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr ""
|
||||
"%(field_list)s フィールドの内容から %(model_name)s を検索するには左のフィール"
|
||||
"ドを使用して下さい。"
|
||||
|
||||
#: db/models.py:15
|
||||
msgid "created"
|
||||
msgstr "作成日時"
|
||||
|
||||
#: db/models.py:16
|
||||
msgid "modified"
|
||||
msgstr "変更日時"
|
||||
|
||||
#: db/models.py:26
|
||||
msgid "title"
|
||||
msgstr "タイトル"
|
||||
|
||||
#: db/models.py:27
|
||||
msgid "slug"
|
||||
msgstr "スラグ"
|
||||
|
||||
#: db/models.py:28
|
||||
msgid "description"
|
||||
msgstr "説明"
|
||||
|
||||
#: db/models.py:50
|
||||
msgid "Inactive"
|
||||
msgstr "非アクティブ"
|
||||
|
||||
#: db/models.py:51
|
||||
msgid "Active"
|
||||
msgstr "アクティブ"
|
||||
|
||||
#: db/models.py:53
|
||||
msgid "status"
|
||||
msgstr "ステータス"
|
||||
|
||||
#: db/models.py:56
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr "すぐに有効化する場合は空白のままにして下さい"
|
||||
|
||||
#: db/models.py:58
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr "無期限に有効化しておく場合は空白のままにして下さい"
|
||||
|
||||
#: management/commands/show_urls.py:34
|
||||
#, python-format
|
||||
msgid "%s does not appear to be a urlpattern object"
|
||||
msgstr "%s は urlpattern オブジェクトではないようです"
|
||||
|
||||
#: templates/django_extensions/widgets/foreignkey_searchinput.html:4
|
||||
msgid "Lookup"
|
||||
msgstr "検索"
|
||||
Binary file not shown.
@@ -0,0 +1,109 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2011-02-02 11:43+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Zbigniew Siciarz <antyqjon@gmail.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: admin/__init__.py:121
|
||||
msgid "and"
|
||||
msgstr "i"
|
||||
|
||||
#: admin/__init__.py:123
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr ""
|
||||
"Użyj pola po lewej, by wyszukać pola %(field_list)s w modelu %(model_name)s."
|
||||
|
||||
#: db/models.py:15
|
||||
msgid "created"
|
||||
msgstr "utworzony"
|
||||
|
||||
#: db/models.py:16
|
||||
msgid "modified"
|
||||
msgstr "zmodyfikowany"
|
||||
|
||||
#: db/models.py:26
|
||||
msgid "title"
|
||||
msgstr "tytuł"
|
||||
|
||||
#: db/models.py:27
|
||||
msgid "slug"
|
||||
msgstr "slug"
|
||||
|
||||
#: db/models.py:28
|
||||
msgid "description"
|
||||
msgstr "opis"
|
||||
|
||||
#: db/models.py:50
|
||||
msgid "Inactive"
|
||||
msgstr "Nieaktywny"
|
||||
|
||||
#: db/models.py:51
|
||||
msgid "Active"
|
||||
msgstr "Aktywny"
|
||||
|
||||
#: db/models.py:53
|
||||
msgid "status"
|
||||
msgstr "stan"
|
||||
|
||||
#: db/models.py:56
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr "pozostaw puste, by aktywować od razu"
|
||||
|
||||
#: db/models.py:58
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr "pozostaw puste, by nie definiować daty deaktywacji"
|
||||
|
||||
#: mongodb/fields/__init__.py:22
|
||||
#, python-format
|
||||
msgid "String (up to %(max_length)s)"
|
||||
msgstr "String (do %(max_length)s znaków)"
|
||||
|
||||
#: management/commands/show_urls.py:34
|
||||
#, python-format
|
||||
msgid "%s does not appear to be a urlpattern object"
|
||||
msgstr "%s nie jest obiektem typu urlpattern"
|
||||
|
||||
#: templates/django_extensions/widgets/foreignkey_searchinput.html:4
|
||||
msgid "Lookup"
|
||||
msgstr "Szukaj"
|
||||
|
||||
#: validators.py:14
|
||||
msgid "Control Characters like new lines or tabs are not allowed."
|
||||
msgstr "Znaki nowej linii i tabulatory nie są dozwolone."
|
||||
|
||||
#: validators.py:48
|
||||
msgid "Leading and Trailing whitespaces are not allowed."
|
||||
msgstr "Białe znaki na początku i końcu wiersza nie są dozwolone."
|
||||
|
||||
#: validators.py:74
|
||||
msgid "Only a hex string is allowed."
|
||||
msgstr "Tylko wartość hex jest dozwolona."
|
||||
|
||||
#: validators.py:75
|
||||
#, python-format
|
||||
msgid "Invalid length. Must be %(length)d characters."
|
||||
msgstr "Niewłaściwa długość. Musi być %(length)d znaków."
|
||||
|
||||
#: validators.py:76
|
||||
#, python-format
|
||||
msgid "Ensure that there are more than %(min)s characters."
|
||||
msgstr "Upewnij się, że jest więcej niż %(min)s znaków."
|
||||
|
||||
#: validators.py:77
|
||||
#, python-format
|
||||
msgid "Ensure that there are no more than %(max)s characters."
|
||||
msgstr "Upewnij się, że nie ma więcej niż %(max)s znaków."
|
||||
Binary file not shown.
@@ -0,0 +1,77 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2011-02-02 11:43+0100\n"
|
||||
"PO-Revision-Date: 2010-11-15 22:06-0300\n"
|
||||
"Last-Translator: Fernando Silva <fernand at liquuid dot net>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: admin/__init__.py:121
|
||||
msgid "and"
|
||||
msgstr "e"
|
||||
|
||||
#: admin/__init__.py:123
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr ""
|
||||
"Use o campo esquerdo para fazer com que o modelo %(model_name)s procure nos "
|
||||
"campos %(field_list)s."
|
||||
|
||||
#: db/models.py:15
|
||||
msgid "created"
|
||||
msgstr "criado"
|
||||
|
||||
#: db/models.py:16
|
||||
msgid "modified"
|
||||
msgstr "modificado"
|
||||
|
||||
#: db/models.py:26
|
||||
msgid "title"
|
||||
msgstr "título"
|
||||
|
||||
#: db/models.py:27
|
||||
msgid "slug"
|
||||
msgstr "slug"
|
||||
|
||||
#: db/models.py:28
|
||||
msgid "description"
|
||||
msgstr "descrição"
|
||||
|
||||
#: db/models.py:50
|
||||
msgid "Inactive"
|
||||
msgstr "Inativo"
|
||||
|
||||
#: db/models.py:51
|
||||
msgid "Active"
|
||||
msgstr "Ativo"
|
||||
|
||||
#: db/models.py:53
|
||||
msgid "status"
|
||||
msgstr "estado"
|
||||
|
||||
#: db/models.py:56
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr "deixe vazio para ativação imediata"
|
||||
|
||||
#: db/models.py:58
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr "deixe vazio para ativação por tempo indeterminado"
|
||||
|
||||
#: management/commands/show_urls.py:34
|
||||
#, python-format
|
||||
msgid "%s does not appear to be a urlpattern object"
|
||||
msgstr "%s não parece ser um objeto urlpattern"
|
||||
|
||||
#: templates/django_extensions/widgets/foreignkey_searchinput.html:4
|
||||
msgid "Lookup"
|
||||
msgstr "Busca"
|
||||
Binary file not shown.
@@ -0,0 +1,79 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# Claudemiro Alves Feitosa Neto <dimiro1@gmail.com>, 2013.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-09-13 22:49-0300\n"
|
||||
"PO-Revision-Date: 2013-09-13 22:49-0300\n"
|
||||
"Last-Translator: Claudemiro Alves Feitosa <dimiro1@gmail.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: pt_BR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: admin/__init__.py:128
|
||||
msgid "and"
|
||||
msgstr "e"
|
||||
|
||||
#: admin/__init__.py:130
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr "Use o campo da esquerda para fazer com que o modelo %(model_name)s procure nos "
|
||||
"campos %(field_list)s"
|
||||
|
||||
#: db/models.py:22 mongodb/models.py:17
|
||||
msgid "created"
|
||||
msgstr "criado"
|
||||
|
||||
#: db/models.py:23 mongodb/models.py:18
|
||||
msgid "modified"
|
||||
msgstr "modificado"
|
||||
|
||||
#: db/models.py:36 mongodb/models.py:29
|
||||
msgid "title"
|
||||
msgstr "título"
|
||||
|
||||
#: db/models.py:37 mongodb/models.py:30
|
||||
msgid "slug"
|
||||
msgstr "slug"
|
||||
|
||||
#: db/models.py:38 mongodb/models.py:31
|
||||
msgid "description"
|
||||
msgstr "descrição"
|
||||
|
||||
#: db/models.py:63 mongodb/models.py:55
|
||||
msgid "Inactive"
|
||||
msgstr "Inativo"
|
||||
|
||||
#: db/models.py:64 mongodb/models.py:56
|
||||
msgid "Active"
|
||||
msgstr "Ativo"
|
||||
|
||||
#: db/models.py:66 mongodb/models.py:58
|
||||
msgid "status"
|
||||
msgstr "status"
|
||||
|
||||
#: db/models.py:67 mongodb/models.py:59
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr "deixe vazio para uma ativação imediata"
|
||||
|
||||
#: db/models.py:68 mongodb/models.py:60
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr "deixe vazio para ativação por tempo indeterminado"
|
||||
|
||||
#: mongodb/fields/__init__.py:24
|
||||
#, python-format
|
||||
msgid "String (up to %(max_length)s)"
|
||||
msgstr "Cadeia de Caracteres (até %(max_length)s)"
|
||||
|
||||
#: templates/django_extensions/widgets/foreignkey_searchinput.html:4
|
||||
msgid "Lookup"
|
||||
msgstr "Busca"
|
||||
Binary file not shown.
@@ -0,0 +1,80 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-extensions\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2011-02-02 11:43+0100\n"
|
||||
"PO-Revision-Date: 2011-02-02 10:38+0000\n"
|
||||
"Last-Translator: Jannis <jannis@leidel.info>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: ro\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < "
|
||||
"20)) ? 1 : 2)\n"
|
||||
|
||||
#: admin/__init__.py:121
|
||||
msgid "and"
|
||||
msgstr "și"
|
||||
|
||||
#: admin/__init__.py:123
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr ""
|
||||
"Folosește câmpul din stânga pentru a efectua căutări de %(model_name)s în "
|
||||
"câmpurile %(field_list)s."
|
||||
|
||||
#: db/models.py:15
|
||||
msgid "created"
|
||||
msgstr "creat"
|
||||
|
||||
#: db/models.py:16
|
||||
msgid "modified"
|
||||
msgstr "modificat"
|
||||
|
||||
#: db/models.py:26
|
||||
msgid "title"
|
||||
msgstr "Titlu"
|
||||
|
||||
#: db/models.py:27
|
||||
msgid "slug"
|
||||
msgstr "Slug"
|
||||
|
||||
#: db/models.py:28
|
||||
msgid "description"
|
||||
msgstr "Descriere"
|
||||
|
||||
#: db/models.py:50
|
||||
msgid "Inactive"
|
||||
msgstr "Inactiv"
|
||||
|
||||
#: db/models.py:51
|
||||
msgid "Active"
|
||||
msgstr "Activ"
|
||||
|
||||
#: db/models.py:53
|
||||
msgid "status"
|
||||
msgstr "Stare"
|
||||
|
||||
#: db/models.py:56
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr "A se lăsa gol pentru activare imediată"
|
||||
|
||||
#: db/models.py:58
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr "A se lăsa gol pentru activare nelimitată"
|
||||
|
||||
#: management/commands/show_urls.py:34
|
||||
#, python-format
|
||||
msgid "%s does not appear to be a urlpattern object"
|
||||
msgstr "%s nu pare să fie un obiect urlpattern"
|
||||
|
||||
#: templates/django_extensions/widgets/foreignkey_searchinput.html:4
|
||||
msgid "Lookup"
|
||||
msgstr "Căutare"
|
||||
Binary file not shown.
@@ -0,0 +1,126 @@
|
||||
# django_extentions in Russian.
|
||||
# django_extensions на Русском.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# Sasha Simkin <sashasimkin@gmail.com>, 2014.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-extensions\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-05-30 14:51-0500\n"
|
||||
"PO-Revision-Date: 2011-02-02 10:42+0000\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
|
||||
|
||||
#: admin/__init__.py:142
|
||||
msgid "and"
|
||||
msgstr "и"
|
||||
|
||||
#: admin/__init__.py:144
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Use the left field to do %(model_name)s lookups in the fields %(field_list)s."
|
||||
msgstr ""
|
||||
"Используйте левое поле, чтобы сделать поиск %(model_name)s в полях "
|
||||
"%(field_list)s."
|
||||
|
||||
#: admin/filter.py:24 admin/filter.py:53
|
||||
msgid "Yes"
|
||||
msgstr "Да"
|
||||
|
||||
#: admin/filter.py:25 admin/filter.py:54
|
||||
msgid "No"
|
||||
msgstr "Нет"
|
||||
|
||||
#: admin/filter.py:32
|
||||
msgid "All"
|
||||
msgstr "Все"
|
||||
|
||||
#: db/models.py:18
|
||||
msgid "created"
|
||||
msgstr "создан"
|
||||
|
||||
#: db/models.py:19
|
||||
msgid "modified"
|
||||
msgstr "изменён"
|
||||
|
||||
#: db/models.py:38
|
||||
msgid "title"
|
||||
msgstr "заголовок"
|
||||
|
||||
#: db/models.py:39
|
||||
msgid "description"
|
||||
msgstr "описание"
|
||||
|
||||
#: db/models.py:60
|
||||
msgid "slug"
|
||||
msgstr "название-метка (Для URL)"
|
||||
|
||||
#: db/models.py:121 mongodb/models.py:76
|
||||
msgid "Inactive"
|
||||
msgstr "Неактивен"
|
||||
|
||||
#: db/models.py:122 mongodb/models.py:77
|
||||
msgid "Active"
|
||||
msgstr "Активен"
|
||||
|
||||
#: db/models.py:124
|
||||
msgid "status"
|
||||
msgstr "статус"
|
||||
|
||||
#: db/models.py:125 mongodb/models.py:80
|
||||
msgid "keep empty for an immediate activation"
|
||||
msgstr "оставьте пустым для немедленной активации"
|
||||
|
||||
#: db/models.py:126 mongodb/models.py:81
|
||||
msgid "keep empty for indefinite activation"
|
||||
msgstr "оставьте пустым для бессрочной активности"
|
||||
|
||||
#: mongodb/fields/__init__.py:22
|
||||
#, python-format
|
||||
msgid "String (up to %(max_length)s)"
|
||||
msgstr "Строка (Не длиннее: %(max_length)s)"
|
||||
|
||||
#: validators.py:14
|
||||
msgid "Control Characters like new lines or tabs are not allowed."
|
||||
msgstr ""
|
||||
"Управляющие символы, такие как символ новой строки и символ табуляции "
|
||||
"недопустимы."
|
||||
|
||||
#: validators.py:48
|
||||
#, fuzzy
|
||||
#| msgid "Leading and Trailing whitespace is not allowed."
|
||||
msgid "Leading and Trailing whitespaces are not allowed."
|
||||
msgstr "Пробел в начале или в конце недопустим."
|
||||
|
||||
#: validators.py:74
|
||||
msgid "Only a hex string is allowed."
|
||||
msgstr "Допустимо использование только шестнадцатеричных строк."
|
||||
|
||||
#: validators.py:75
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Invalid length must be %(length)d characters."
|
||||
msgid "Invalid length. Must be %(length)d characters."
|
||||
msgstr "Недопустимая длина, должно быть %(length)d символов."
|
||||
|
||||
#: validators.py:76
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Ensure that there are more then %(min)s characters."
|
||||
msgid "Ensure that there are more than %(min)s characters."
|
||||
msgstr "Убедитесь, что длина строки больше %(min)s символов."
|
||||
|
||||
#: validators.py:77
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Ensure that there are no more then %(max)s characters."
|
||||
msgid "Ensure that there are no more than %(max)s characters."
|
||||
msgstr "Убедитесь, что длина строки не больше %(max)s символов."
|
||||
|
||||
#~ msgid "Lookup"
|
||||
#~ msgstr "Поиск"
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import time
|
||||
import logging
|
||||
from hashlib import md5
|
||||
|
||||
# also see: https://djangosnippets.org/snippets/2242/
|
||||
|
||||
|
||||
class RateLimiterFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
||||
# Rate is specified as 1 messages logged per N seconds. (aka cache timeout)
|
||||
rate = getattr(settings, "RATE_LIMITER_FILTER_RATE", 10)
|
||||
prefix = getattr(settings, "RATE_LIMITER_FILTER_PREFIX", "ratelimiterfilter")
|
||||
|
||||
subject = record.getMessage()
|
||||
cache_key = "%s:%s" % (prefix, md5(subject).hexdigest())
|
||||
cache_count_key = "%s:count" % cache_key
|
||||
|
||||
result = cache.get_many([cache_key, cache_count_key])
|
||||
value = result.get(cache_key)
|
||||
cntr = result.get(cache_count_key)
|
||||
|
||||
if not cntr:
|
||||
cntr = 1
|
||||
cache.set(cache_count_key, cntr, rate + 60)
|
||||
|
||||
if value:
|
||||
cache.incr(cache_count_key)
|
||||
return False
|
||||
|
||||
record.msg = "[%sx] %s" % (cntr, record.msg)
|
||||
cache.set(cache_key, time.time(), rate)
|
||||
return True
|
||||
@@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from logging import getLogger
|
||||
|
||||
logger = getLogger("django.commands")
|
||||
|
||||
|
||||
class LoggingBaseCommand(BaseCommand):
|
||||
"""
|
||||
A subclass of BaseCommand that logs run time errors to `django.commands`.
|
||||
To use this, create a management command subclassing LoggingBaseCommand:
|
||||
|
||||
from django_extensions.management.base import LoggingBaseCommand
|
||||
|
||||
class Command(LoggingBaseCommand):
|
||||
help = 'Test error'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
raise Exception
|
||||
|
||||
|
||||
And then define a logging handler in settings.py:
|
||||
|
||||
LOGGING = {
|
||||
... # Other stuff here
|
||||
|
||||
'handlers': {
|
||||
'mail_admins': {
|
||||
'level': 'ERROR',
|
||||
'filters': ['require_debug_false'],
|
||||
'class': 'django.utils.log.AdminEmailHandler'
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'django.commands': {
|
||||
'handlers': ['mail_admins'],
|
||||
'level': 'ERROR',
|
||||
'propagate': False,
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
def execute(self, *args, **options):
|
||||
try:
|
||||
super().execute(*args, **options)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=sys.exc_info(), extra={"status_code": 500})
|
||||
raise
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.core.management import color
|
||||
from django.utils import termcolors
|
||||
|
||||
|
||||
def _dummy_style_func(msg):
|
||||
return msg
|
||||
|
||||
|
||||
def no_style():
|
||||
style = color.no_style()
|
||||
for role in ("INFO", "WARN", "BOLD", "URL", "MODULE", "MODULE_NAME", "URL_NAME"):
|
||||
setattr(style, role, _dummy_style_func)
|
||||
return style
|
||||
|
||||
|
||||
def color_style():
|
||||
if color.supports_color():
|
||||
style = color.color_style()
|
||||
style.INFO = termcolors.make_style(fg="green")
|
||||
style.WARN = termcolors.make_style(fg="yellow")
|
||||
style.BOLD = termcolors.make_style(opts=("bold",))
|
||||
style.URL = termcolors.make_style(fg="green", opts=("bold",))
|
||||
style.MODULE = termcolors.make_style(fg="yellow")
|
||||
style.MODULE_NAME = termcolors.make_style(opts=("bold",))
|
||||
style.URL_NAME = termcolors.make_style(fg="red")
|
||||
else:
|
||||
style = no_style()
|
||||
return style
|
||||
@@ -0,0 +1,395 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
The Django Admin Generator is a project which can automatically generate
|
||||
(scaffold) a Django Admin for you. By doing this it will introspect your
|
||||
models and automatically generate an Admin with properties like:
|
||||
|
||||
- `list_display` for all local fields
|
||||
- `list_filter` for foreign keys with few items
|
||||
- `raw_id_fields` for foreign keys with a lot of items
|
||||
- `search_fields` for name and `slug` fields
|
||||
- `prepopulated_fields` for `slug` fields
|
||||
- `date_hierarchy` for `created_at`, `updated_at` or `joined_at` fields
|
||||
|
||||
The original source and latest version can be found here:
|
||||
https://github.com/WoLpH/django-admin-generator/
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.management.base import LabelCommand, CommandError
|
||||
from django.db import models
|
||||
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
# Configurable constants
|
||||
MAX_LINE_WIDTH = getattr(settings, "MAX_LINE_WIDTH", 78)
|
||||
INDENT_WIDTH = getattr(settings, "INDENT_WIDTH", 4)
|
||||
LIST_FILTER_THRESHOLD = getattr(settings, "LIST_FILTER_THRESHOLD", 25)
|
||||
RAW_ID_THRESHOLD = getattr(settings, "RAW_ID_THRESHOLD", 100)
|
||||
|
||||
LIST_FILTER = getattr(
|
||||
settings,
|
||||
"LIST_FILTER",
|
||||
(
|
||||
models.DateField,
|
||||
models.DateTimeField,
|
||||
models.ForeignKey,
|
||||
models.BooleanField,
|
||||
),
|
||||
)
|
||||
|
||||
SEARCH_FIELD_NAMES = getattr(
|
||||
settings,
|
||||
"SEARCH_FIELD_NAMES",
|
||||
(
|
||||
"name",
|
||||
"slug",
|
||||
),
|
||||
)
|
||||
|
||||
DATE_HIERARCHY_NAMES = getattr(
|
||||
settings,
|
||||
"DATE_HIERARCHY_NAMES",
|
||||
(
|
||||
"joined_at",
|
||||
"updated_at",
|
||||
"created_at",
|
||||
),
|
||||
)
|
||||
|
||||
PREPOPULATED_FIELD_NAMES = getattr(settings, "PREPOPULATED_FIELD_NAMES", ("slug=name",))
|
||||
|
||||
PRINT_IMPORTS = getattr(
|
||||
settings,
|
||||
"PRINT_IMPORTS",
|
||||
"""# -*- coding: utf-8 -*-
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import %(models)s
|
||||
""",
|
||||
)
|
||||
|
||||
PRINT_ADMIN_CLASS = getattr(
|
||||
settings,
|
||||
"PRINT_ADMIN_CLASS",
|
||||
"""
|
||||
|
||||
@admin.register(%(name)s)
|
||||
class %(name)sAdmin(admin.ModelAdmin):%(class_)s
|
||||
""",
|
||||
)
|
||||
|
||||
PRINT_ADMIN_PROPERTY = getattr(
|
||||
settings,
|
||||
"PRINT_ADMIN_PROPERTY",
|
||||
"""
|
||||
%(key)s = %(value)s""",
|
||||
)
|
||||
|
||||
|
||||
class UnicodeMixin:
|
||||
"""
|
||||
Mixin class to handle defining the proper __str__/__unicode__
|
||||
methods in Python 2 or 3.
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return self.__unicode__()
|
||||
|
||||
|
||||
class AdminApp(UnicodeMixin):
|
||||
def __init__(self, app_config, model_res, **options):
|
||||
self.app_config = app_config
|
||||
self.model_res = model_res
|
||||
self.options = options
|
||||
|
||||
def __iter__(self):
|
||||
for model in self.app_config.get_models():
|
||||
admin_model = AdminModel(model, **self.options)
|
||||
|
||||
for model_re in self.model_res:
|
||||
if model_re.search(admin_model.name):
|
||||
break
|
||||
else:
|
||||
if self.model_res:
|
||||
continue
|
||||
|
||||
yield admin_model
|
||||
|
||||
def __unicode__(self):
|
||||
return "".join(self._unicode_generator())
|
||||
|
||||
def _unicode_generator(self):
|
||||
models_list = [admin_model.name for admin_model in self]
|
||||
yield PRINT_IMPORTS % dict(models=", ".join(models_list))
|
||||
|
||||
admin_model_names = []
|
||||
for admin_model in self:
|
||||
yield PRINT_ADMIN_CLASS % dict(
|
||||
name=admin_model.name,
|
||||
class_=admin_model,
|
||||
)
|
||||
admin_model_names.append(admin_model.name)
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s[%s]>" % (
|
||||
self.__class__.__name__,
|
||||
self.app.name,
|
||||
)
|
||||
|
||||
|
||||
class AdminModel(UnicodeMixin):
|
||||
PRINTABLE_PROPERTIES = (
|
||||
"list_display",
|
||||
"list_filter",
|
||||
"raw_id_fields",
|
||||
"search_fields",
|
||||
"prepopulated_fields",
|
||||
"date_hierarchy",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
raw_id_threshold=RAW_ID_THRESHOLD,
|
||||
list_filter_threshold=LIST_FILTER_THRESHOLD,
|
||||
search_field_names=SEARCH_FIELD_NAMES,
|
||||
date_hierarchy_names=DATE_HIERARCHY_NAMES,
|
||||
prepopulated_field_names=PREPOPULATED_FIELD_NAMES,
|
||||
**options,
|
||||
):
|
||||
self.model = model
|
||||
self.list_display = []
|
||||
self.list_filter = []
|
||||
self.raw_id_fields = []
|
||||
self.search_fields = []
|
||||
self.prepopulated_fields = {}
|
||||
self.date_hierarchy = None
|
||||
self.search_field_names = search_field_names
|
||||
self.raw_id_threshold = raw_id_threshold
|
||||
self.list_filter_threshold = list_filter_threshold
|
||||
self.date_hierarchy_names = date_hierarchy_names
|
||||
self.prepopulated_field_names = prepopulated_field_names
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s[%s]>" % (
|
||||
self.__class__.__name__,
|
||||
self.name,
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.model.__name__
|
||||
|
||||
def _process_many_to_many(self, meta):
|
||||
raw_id_threshold = self.raw_id_threshold
|
||||
for field in meta.local_many_to_many:
|
||||
if hasattr(field, "remote_field"):
|
||||
related_model = getattr(
|
||||
field.remote_field, "related_model", field.remote_field.model
|
||||
)
|
||||
else:
|
||||
raise CommandError("Unable to process ManyToMany relation")
|
||||
related_objects = related_model.objects.all()
|
||||
if related_objects[:raw_id_threshold].count() < raw_id_threshold:
|
||||
yield field.name
|
||||
|
||||
def _process_fields(self, meta):
|
||||
parent_fields = meta.parents.values()
|
||||
for field in meta.fields:
|
||||
name = self._process_field(field, parent_fields)
|
||||
if name:
|
||||
yield name
|
||||
|
||||
def _process_foreign_key(self, field):
|
||||
raw_id_threshold = self.raw_id_threshold
|
||||
list_filter_threshold = self.list_filter_threshold
|
||||
max_count = max(list_filter_threshold, raw_id_threshold)
|
||||
if hasattr(field, "remote_field"):
|
||||
related_model = getattr(
|
||||
field.remote_field, "related_model", field.remote_field.model
|
||||
)
|
||||
else:
|
||||
raise CommandError("Unable to process ForeignKey relation")
|
||||
related_count = related_model.objects.all()
|
||||
related_count = related_count[:max_count].count()
|
||||
|
||||
if related_count >= raw_id_threshold:
|
||||
self.raw_id_fields.append(field.name)
|
||||
|
||||
elif related_count < list_filter_threshold:
|
||||
self.list_filter.append(field.name)
|
||||
|
||||
else: # pragma: no cover
|
||||
pass # Do nothing :)
|
||||
|
||||
def _process_field(self, field, parent_fields):
|
||||
if field in parent_fields:
|
||||
return
|
||||
|
||||
field_name = str(field.name)
|
||||
self.list_display.append(field_name)
|
||||
if isinstance(field, LIST_FILTER):
|
||||
if isinstance(field, models.ForeignKey):
|
||||
self._process_foreign_key(field)
|
||||
else:
|
||||
self.list_filter.append(field_name)
|
||||
|
||||
if field.name in self.search_field_names:
|
||||
self.search_fields.append(field_name)
|
||||
|
||||
return field_name
|
||||
|
||||
def __unicode__(self):
|
||||
return "".join(self._unicode_generator())
|
||||
|
||||
def _yield_value(self, key, value):
|
||||
if isinstance(value, (list, set, tuple)):
|
||||
return self._yield_tuple(key, tuple(value))
|
||||
elif isinstance(value, dict):
|
||||
return self._yield_dict(key, value)
|
||||
elif isinstance(value, str):
|
||||
return self._yield_string(key, value)
|
||||
else: # pragma: no cover
|
||||
raise TypeError("%s is not supported in %r" % (type(value), value))
|
||||
|
||||
def _yield_string(self, key, value, converter=repr):
|
||||
return PRINT_ADMIN_PROPERTY % dict(
|
||||
key=key,
|
||||
value=converter(value),
|
||||
)
|
||||
|
||||
def _yield_dict(self, key, value):
|
||||
row_parts = []
|
||||
row = self._yield_string(key, value)
|
||||
if len(row) > MAX_LINE_WIDTH:
|
||||
row_parts.append(self._yield_string(key, "{", str))
|
||||
for k, v in value.items():
|
||||
row_parts.append("%s%r: %r" % (2 * INDENT_WIDTH * " ", k, v))
|
||||
|
||||
row_parts.append(INDENT_WIDTH * " " + "}")
|
||||
row = "\n".join(row_parts)
|
||||
|
||||
return row
|
||||
|
||||
def _yield_tuple(self, key, value):
|
||||
row_parts = []
|
||||
row = self._yield_string(key, value)
|
||||
if len(row) > MAX_LINE_WIDTH:
|
||||
row_parts.append(self._yield_string(key, "(", str))
|
||||
for v in value:
|
||||
row_parts.append(2 * INDENT_WIDTH * " " + repr(v) + ",")
|
||||
|
||||
row_parts.append(INDENT_WIDTH * " " + ")")
|
||||
row = "\n".join(row_parts)
|
||||
|
||||
return row
|
||||
|
||||
def _unicode_generator(self):
|
||||
self._process()
|
||||
for key in self.PRINTABLE_PROPERTIES:
|
||||
value = getattr(self, key)
|
||||
if value:
|
||||
yield self._yield_value(key, value)
|
||||
|
||||
def _process(self):
|
||||
meta = self.model._meta
|
||||
|
||||
self.raw_id_fields += list(self._process_many_to_many(meta))
|
||||
field_names = list(self._process_fields(meta))
|
||||
|
||||
for field_name in self.date_hierarchy_names[::-1]:
|
||||
if field_name in field_names and not self.date_hierarchy:
|
||||
self.date_hierarchy = field_name
|
||||
break
|
||||
|
||||
for k in sorted(self.prepopulated_field_names):
|
||||
k, vs = k.split("=", 1)
|
||||
vs = vs.split(",")
|
||||
if k in field_names:
|
||||
incomplete = False
|
||||
for v in vs:
|
||||
if v not in field_names:
|
||||
incomplete = True
|
||||
break
|
||||
|
||||
if not incomplete:
|
||||
self.prepopulated_fields[k] = vs
|
||||
|
||||
self.processed = True
|
||||
|
||||
|
||||
class Command(LabelCommand):
|
||||
help = """Generate a `admin.py` file for the given app (models)"""
|
||||
# args = "[app_name]"
|
||||
can_import_settings = True
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("app_name")
|
||||
parser.add_argument("model_name", nargs="*")
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--search-field",
|
||||
action="append",
|
||||
default=SEARCH_FIELD_NAMES,
|
||||
help="Fields named like this will be added to `search_fields`"
|
||||
" [default: %(default)s]",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--date-hierarchy",
|
||||
action="append",
|
||||
default=DATE_HIERARCHY_NAMES,
|
||||
help="A field named like this will be set as `date_hierarchy`"
|
||||
" [default: %(default)s]",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--prepopulated-fields",
|
||||
action="append",
|
||||
default=PREPOPULATED_FIELD_NAMES,
|
||||
help="These fields will be prepopulated by the other field."
|
||||
"The field names can be specified like `spam=eggA,eggB,eggC`"
|
||||
" [default: %(default)s]",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--list-filter-threshold",
|
||||
type=int,
|
||||
default=LIST_FILTER_THRESHOLD,
|
||||
metavar="LIST_FILTER_THRESHOLD",
|
||||
help="If a foreign key has less than LIST_FILTER_THRESHOLD items "
|
||||
"it will be added to `list_filter` [default: %(default)s]",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--raw-id-threshold",
|
||||
type=int,
|
||||
default=RAW_ID_THRESHOLD,
|
||||
metavar="RAW_ID_THRESHOLD",
|
||||
help="If a foreign key has more than RAW_ID_THRESHOLD items "
|
||||
"it will be added to `list_filter` [default: %(default)s]",
|
||||
)
|
||||
|
||||
@signalcommand
|
||||
def handle(self, *args, **options):
|
||||
app_name = options["app_name"]
|
||||
|
||||
try:
|
||||
app = apps.get_app_config(app_name)
|
||||
except LookupError:
|
||||
self.stderr.write("This command requires an existing app name as argument")
|
||||
self.stderr.write("Available apps:")
|
||||
app_labels = [app.label for app in apps.get_app_configs()]
|
||||
for label in sorted(app_labels):
|
||||
self.stderr.write(" %s" % label)
|
||||
return
|
||||
|
||||
model_res = []
|
||||
for arg in options["model_name"]:
|
||||
model_res.append(re.compile(arg, re.IGNORECASE))
|
||||
|
||||
self.stdout.write(AdminApp(app, model_res, **options).__str__())
|
||||
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import fnmatch
|
||||
import os
|
||||
from os.path import join as _j
|
||||
from typing import List
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Removes all python bytecode compiled files from the project."
|
||||
|
||||
requires_system_checks: List[str] = []
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--optimize",
|
||||
"-o",
|
||||
"-O",
|
||||
action="store_true",
|
||||
dest="optimize",
|
||||
default=False,
|
||||
help="Remove optimized python bytecode files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--path",
|
||||
"-p",
|
||||
action="store",
|
||||
dest="path",
|
||||
help="Specify path to recurse into",
|
||||
)
|
||||
|
||||
@signalcommand
|
||||
def handle(self, *args, **options):
|
||||
project_root = options.get("path", getattr(settings, "BASE_DIR", None))
|
||||
if not project_root:
|
||||
project_root = getattr(settings, "BASE_DIR", None)
|
||||
|
||||
verbosity = options["verbosity"]
|
||||
if not project_root:
|
||||
raise CommandError(
|
||||
"No --path specified and settings.py does not contain BASE_DIR"
|
||||
)
|
||||
|
||||
exts = options["optimize"] and "*.py[co]" or "*.pyc"
|
||||
|
||||
for root, dirs, filenames in os.walk(project_root):
|
||||
for filename in fnmatch.filter(filenames, exts):
|
||||
full_path = _j(root, filename)
|
||||
if verbosity > 1:
|
||||
self.stdout.write("%s\n" % full_path)
|
||||
os.remove(full_path)
|
||||
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Author: AxiaCore S.A.S. https://axiacore.com
|
||||
from django.conf import settings
|
||||
from django.core.cache import DEFAULT_CACHE_ALIAS, caches
|
||||
from django.core.cache.backends.base import InvalidCacheBackendError
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""A simple management command which clears the site-wide cache."""
|
||||
|
||||
help = "Fully clear site-wide cache."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--cache", action="append", help="Name of cache to clear")
|
||||
parser.add_argument(
|
||||
"--all",
|
||||
"-a",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="all_caches",
|
||||
help="Clear all configured caches",
|
||||
)
|
||||
|
||||
@signalcommand
|
||||
def handle(self, cache, all_caches, *args, **kwargs):
|
||||
if not cache and not all_caches:
|
||||
cache = [DEFAULT_CACHE_ALIAS]
|
||||
elif cache and all_caches:
|
||||
raise CommandError("Using both --all and --cache is not supported")
|
||||
elif all_caches:
|
||||
cache = getattr(settings, "CACHES", {DEFAULT_CACHE_ALIAS: {}}).keys()
|
||||
|
||||
for key in cache:
|
||||
try:
|
||||
caches[key].clear()
|
||||
except InvalidCacheBackendError:
|
||||
self.stderr.write('Cache "%s" is invalid!\n' % key)
|
||||
else:
|
||||
self.stdout.write('Cache "%s" has been cleared!\n' % key)
|
||||
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import fnmatch
|
||||
import os
|
||||
import py_compile
|
||||
from os.path import join as _j
|
||||
from typing import List
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Compile python bytecode files for the project."
|
||||
requires_system_checks: List[str] = []
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--path",
|
||||
"-p",
|
||||
action="store",
|
||||
dest="path",
|
||||
help="Specify path to recurse into",
|
||||
)
|
||||
|
||||
@signalcommand
|
||||
def handle(self, *args, **options):
|
||||
project_root = options["path"]
|
||||
if not project_root:
|
||||
project_root = getattr(settings, "BASE_DIR", None)
|
||||
|
||||
verbosity = options["verbosity"]
|
||||
if not project_root:
|
||||
raise CommandError(
|
||||
"No --path specified and settings.py does not contain BASE_DIR"
|
||||
)
|
||||
|
||||
for root, dirs, filenames in os.walk(project_root):
|
||||
for filename in fnmatch.filter(filenames, "*.py"):
|
||||
full_path = _j(root, filename)
|
||||
if verbosity > 1:
|
||||
self.stdout.write("Compiling %s...\n" % full_path)
|
||||
py_compile.compile(full_path)
|
||||
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
from typing import List
|
||||
|
||||
from django.core.management.base import AppCommand
|
||||
from django.core.management.color import color_style
|
||||
|
||||
from django_extensions.management.utils import _make_writeable, signalcommand
|
||||
|
||||
|
||||
class Command(AppCommand):
|
||||
help = "Creates a Django management command directory structure for the given app "
|
||||
"name in the app's directory."
|
||||
|
||||
requires_system_checks: List[str] = []
|
||||
# Can't import settings during this command, because they haven't
|
||||
# necessarily been created.
|
||||
can_import_settings = True
|
||||
|
||||
def add_arguments(self, parser):
|
||||
super().add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
"-n",
|
||||
action="store",
|
||||
dest="command_name",
|
||||
default="sample",
|
||||
help="The name to use for the management command",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--base",
|
||||
"-b",
|
||||
action="store",
|
||||
dest="base_command",
|
||||
default="Base",
|
||||
help="The base class used for implementation of "
|
||||
"this command. Should be one of Base, App, Label, or NoArgs",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Do not actually create any files",
|
||||
)
|
||||
|
||||
@signalcommand
|
||||
def handle_app_config(self, args, **options):
|
||||
app = args
|
||||
copy_template("command_template", app.path, **options)
|
||||
|
||||
|
||||
def copy_template(template_name, copy_to, **options):
|
||||
"""Copy the specified template directory to the copy_to location"""
|
||||
import django_extensions
|
||||
|
||||
style = color_style()
|
||||
ERROR = getattr(style, "ERROR", lambda x: x)
|
||||
SUCCESS = getattr(style, "SUCCESS", lambda x: x)
|
||||
|
||||
command_name, base_command = (
|
||||
options["command_name"],
|
||||
"%sCommand" % options["base_command"],
|
||||
)
|
||||
dry_run = options["dry_run"]
|
||||
verbosity = options["verbosity"]
|
||||
|
||||
template_dir = os.path.join(django_extensions.__path__[0], "conf", template_name)
|
||||
|
||||
# walk the template structure and copies it
|
||||
for d, subdirs, files in os.walk(template_dir):
|
||||
relative_dir = d[len(template_dir) + 1 :]
|
||||
if relative_dir and not os.path.exists(os.path.join(copy_to, relative_dir)):
|
||||
if not dry_run:
|
||||
os.mkdir(os.path.join(copy_to, relative_dir))
|
||||
for i, subdir in enumerate(subdirs):
|
||||
if subdir.startswith("."):
|
||||
del subdirs[i]
|
||||
for f in files:
|
||||
if f.endswith((".pyc", ".pyo")) or f.startswith(
|
||||
(".DS_Store", "__pycache__")
|
||||
):
|
||||
continue
|
||||
path_old = os.path.join(d, f)
|
||||
path_new = os.path.join(
|
||||
copy_to, relative_dir, f.replace("sample", command_name)
|
||||
).rstrip(".tmpl")
|
||||
if os.path.exists(path_new):
|
||||
path_new = os.path.join(copy_to, relative_dir, f).rstrip(".tmpl")
|
||||
if os.path.exists(path_new):
|
||||
if verbosity > 1:
|
||||
print(ERROR("%s already exists" % path_new))
|
||||
continue
|
||||
if verbosity > 1:
|
||||
print(SUCCESS("%s" % path_new))
|
||||
with open(path_old, "r") as fp_orig:
|
||||
data = fp_orig.read()
|
||||
data = data.replace("{{ command_name }}", command_name)
|
||||
data = data.replace("{{ base_command }}", base_command)
|
||||
if not dry_run:
|
||||
with open(path_new, "w") as fp_new:
|
||||
fp_new.write(data)
|
||||
if not dry_run:
|
||||
try:
|
||||
shutil.copymode(path_old, path_new)
|
||||
_make_writeable(path_new)
|
||||
except OSError:
|
||||
sys.stderr.write(
|
||||
"Notice: Couldn't set permission bits on %s. You're probably using an uncommon filesystem setup. No problem.\n" # noqa: E501
|
||||
% path_new
|
||||
)
|
||||
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
from typing import List
|
||||
|
||||
from django.core.management.base import AppCommand
|
||||
from django.core.management.color import color_style
|
||||
|
||||
from django_extensions.management.utils import _make_writeable, signalcommand
|
||||
|
||||
|
||||
class Command(AppCommand):
|
||||
help = "Creates a Django jobs command directory structure for the given app name "
|
||||
"in the current directory."
|
||||
|
||||
requires_system_checks: List[str] = []
|
||||
# Can't import settings during this command, because they haven't
|
||||
# necessarily been created.
|
||||
can_import_settings = True
|
||||
|
||||
@signalcommand
|
||||
def handle_app_config(self, app, **options):
|
||||
copy_template("jobs_template", app.path, **options)
|
||||
|
||||
|
||||
def copy_template(template_name, copy_to, **options):
|
||||
"""Copy the specified template directory to the copy_to location"""
|
||||
import django_extensions
|
||||
|
||||
style = color_style()
|
||||
ERROR = getattr(style, "ERROR", lambda x: x)
|
||||
SUCCESS = getattr(style, "SUCCESS", lambda x: x)
|
||||
|
||||
template_dir = os.path.join(django_extensions.__path__[0], "conf", template_name)
|
||||
verbosity = options["verbosity"]
|
||||
|
||||
# walks the template structure and copies it
|
||||
for d, subdirs, files in os.walk(template_dir):
|
||||
relative_dir = d[len(template_dir) + 1 :]
|
||||
if relative_dir and not os.path.exists(os.path.join(copy_to, relative_dir)):
|
||||
os.mkdir(os.path.join(copy_to, relative_dir))
|
||||
for i, subdir in enumerate(subdirs):
|
||||
if subdir.startswith("."):
|
||||
del subdirs[i]
|
||||
for f in files:
|
||||
if f.endswith(".pyc") or f.startswith(".DS_Store"):
|
||||
continue
|
||||
path_old = os.path.join(d, f)
|
||||
path_new = os.path.join(copy_to, relative_dir, f).rstrip(".tmpl")
|
||||
if os.path.exists(path_new):
|
||||
if verbosity > 1:
|
||||
print(ERROR("%s already exists" % path_new))
|
||||
continue
|
||||
if verbosity > 1:
|
||||
print(SUCCESS("%s" % path_new))
|
||||
|
||||
with open(path_old, "r") as fp_orig:
|
||||
with open(path_new, "w") as fp_new:
|
||||
fp_new.write(fp_orig.read())
|
||||
|
||||
try:
|
||||
shutil.copymode(path_old, path_new)
|
||||
_make_writeable(path_new)
|
||||
except OSError:
|
||||
sys.stderr.write(
|
||||
"Notice: Couldn't set permission bits on %s. You're probably using an uncommon filesystem setup. No problem.\n" # noqa: E501
|
||||
% path_new
|
||||
)
|
||||
@@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
from django.core.management.base import AppCommand
|
||||
|
||||
from django_extensions.management.utils import _make_writeable, signalcommand
|
||||
|
||||
|
||||
class Command(AppCommand):
|
||||
help = "Creates a Django template tags directory structure for the given app name "
|
||||
"in the apps's directory"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
super().add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
"-n",
|
||||
action="store",
|
||||
dest="tag_library_name",
|
||||
default="appname_tags",
|
||||
help="The name to use for the template tag base name. "
|
||||
"Defaults to `appname`_tags.",
|
||||
)
|
||||
|
||||
requires_system_checks: List[str] = []
|
||||
# Can't import settings during this command, because they haven't
|
||||
# necessarily been created.
|
||||
can_import_settings = True
|
||||
|
||||
@signalcommand
|
||||
def handle_app_config(self, app_config, **options):
|
||||
app_dir = app_config.path
|
||||
tag_library_name = options["tag_library_name"]
|
||||
if tag_library_name == "appname_tags":
|
||||
tag_library_name = "%s_tags" % os.path.basename(app_dir)
|
||||
copy_template("template_tags_template", app_dir, tag_library_name)
|
||||
|
||||
|
||||
def copy_template(template_name, copy_to, tag_library_name):
|
||||
"""Copy the specified template directory to the copy_to location"""
|
||||
import django_extensions
|
||||
import shutil
|
||||
|
||||
template_dir = os.path.join(django_extensions.__path__[0], "conf", template_name)
|
||||
|
||||
# walk the template structure and copies it
|
||||
for d, subdirs, files in os.walk(template_dir):
|
||||
relative_dir = d[len(template_dir) + 1 :]
|
||||
if relative_dir and not os.path.exists(os.path.join(copy_to, relative_dir)):
|
||||
os.mkdir(os.path.join(copy_to, relative_dir))
|
||||
for i, subdir in enumerate(subdirs):
|
||||
if subdir.startswith("."):
|
||||
del subdirs[i]
|
||||
for f in files:
|
||||
if f.endswith(".pyc") or f.startswith(".DS_Store"):
|
||||
continue
|
||||
path_old = os.path.join(d, f)
|
||||
path_new = os.path.join(
|
||||
copy_to, relative_dir, f.replace("sample", tag_library_name)
|
||||
)
|
||||
if os.path.exists(path_new):
|
||||
path_new = os.path.join(copy_to, relative_dir, f)
|
||||
if os.path.exists(path_new):
|
||||
continue
|
||||
path_new = path_new.rstrip(".tmpl")
|
||||
fp_old = open(path_old, "r")
|
||||
fp_new = open(path_new, "w")
|
||||
fp_new.write(fp_old.read())
|
||||
fp_old.close()
|
||||
fp_new.close()
|
||||
try:
|
||||
shutil.copymode(path_old, path_new)
|
||||
_make_writeable(path_new)
|
||||
except OSError:
|
||||
sys.stderr.write(
|
||||
"Notice: Couldn't set permission bits on %s. You're probably using an uncommon filesystem setup. No problem.\n" # noqa: E501
|
||||
% path_new
|
||||
)
|
||||
@@ -0,0 +1,214 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import inspect
|
||||
import re
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import DEFAULT_DB_ALIAS, connections
|
||||
from django.db.migrations.loader import AmbiguityError, MigrationLoader
|
||||
|
||||
REPLACES_REGEX = re.compile(r"\s+replaces\s*=\s*\[[^\]]+\]\s*")
|
||||
PYC = ".pyc"
|
||||
|
||||
|
||||
def py_from_pyc(pyc_fn):
|
||||
return pyc_fn[: -len(PYC)] + ".py"
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Deletes left over migrations that have been replaced by a "
|
||||
"squashed migration and converts squashed migration into a normal "
|
||||
"migration. Modifies your source tree! Use with care!"
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"app_label",
|
||||
help="App label of the application to delete replaced migrations from.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"squashed_migration_name",
|
||||
default=None,
|
||||
nargs="?",
|
||||
help="The squashed migration to replace. "
|
||||
"If not specified defaults to the first found.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--noinput",
|
||||
"--no-input",
|
||||
action="store_false",
|
||||
dest="interactive",
|
||||
default=True,
|
||||
help="Tells Django to NOT prompt the user for input of any kind.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Do not actually delete or change any files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
help=(
|
||||
"Nominates a database to run command for. "
|
||||
'Defaults to the "%s" database.'
|
||||
)
|
||||
% DEFAULT_DB_ALIAS,
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
self.verbosity = options["verbosity"]
|
||||
self.interactive = options["interactive"]
|
||||
self.dry_run = options["dry_run"]
|
||||
app_label = options["app_label"]
|
||||
squashed_migration_name = options["squashed_migration_name"]
|
||||
database = options["database"]
|
||||
|
||||
# Load the current graph state
|
||||
# check the app and migration they asked for exists
|
||||
loader = MigrationLoader(connections[database])
|
||||
if app_label not in loader.migrated_apps:
|
||||
raise CommandError(
|
||||
"App '%s' does not have migrations (so delete_squashed_migrations on "
|
||||
"it makes no sense)" % app_label
|
||||
)
|
||||
|
||||
squashed_migration = None
|
||||
if squashed_migration_name:
|
||||
squashed_migration = self.find_migration(
|
||||
loader, app_label, squashed_migration_name
|
||||
)
|
||||
if not squashed_migration.replaces:
|
||||
raise CommandError(
|
||||
"The migration %s %s is not a squashed migration."
|
||||
% (squashed_migration.app_label, squashed_migration.name)
|
||||
)
|
||||
else:
|
||||
leaf_nodes = loader.graph.leaf_nodes(app=app_label)
|
||||
migration = loader.get_migration(*leaf_nodes[0])
|
||||
previous_migrations = [
|
||||
loader.get_migration(al, mn)
|
||||
for al, mn in loader.graph.forwards_plan(
|
||||
(migration.app_label, migration.name)
|
||||
)
|
||||
if al == migration.app_label
|
||||
]
|
||||
migrations = previous_migrations + [migration]
|
||||
for migration in migrations:
|
||||
if migration.replaces:
|
||||
squashed_migration = migration
|
||||
break
|
||||
|
||||
if not squashed_migration:
|
||||
raise CommandError(
|
||||
"Cannot find a squashed migration in app '%s'." % (app_label)
|
||||
)
|
||||
|
||||
files_to_delete = []
|
||||
for al, mn in squashed_migration.replaces:
|
||||
try:
|
||||
migration = loader.disk_migrations[al, mn]
|
||||
except KeyError:
|
||||
if self.verbosity > 0:
|
||||
self.stderr.write(
|
||||
"Couldn't find migration file for %s %s\n" % (al, mn)
|
||||
)
|
||||
else:
|
||||
pyc_file = inspect.getfile(migration.__class__)
|
||||
files_to_delete.append(pyc_file)
|
||||
if pyc_file.endswith(PYC):
|
||||
py_file = py_from_pyc(pyc_file)
|
||||
files_to_delete.append(py_file)
|
||||
|
||||
# Tell them what we're doing and optionally ask if we should proceed
|
||||
if self.verbosity > 0 or self.interactive:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_HEADING("Will delete the following files:")
|
||||
)
|
||||
for fn in files_to_delete:
|
||||
self.stdout.write(" - %s" % fn)
|
||||
|
||||
if not self.confirm():
|
||||
return
|
||||
|
||||
for fn in files_to_delete:
|
||||
try:
|
||||
if not self.dry_run:
|
||||
os.remove(fn)
|
||||
except OSError:
|
||||
if self.verbosity > 0:
|
||||
self.stderr.write("Couldn't delete %s\n" % (fn,))
|
||||
|
||||
# Try and delete replaces only if it's all on one line
|
||||
squashed_migration_fn = inspect.getfile(squashed_migration.__class__)
|
||||
if squashed_migration_fn.endswith(PYC):
|
||||
squashed_migration_fn = py_from_pyc(squashed_migration_fn)
|
||||
with open(squashed_migration_fn) as fp:
|
||||
squashed_migration_lines = list(fp)
|
||||
|
||||
delete_lines = []
|
||||
for i, line in enumerate(squashed_migration_lines):
|
||||
if REPLACES_REGEX.match(line):
|
||||
delete_lines.append(i)
|
||||
if i > 0 and squashed_migration_lines[i - 1].strip() == "":
|
||||
delete_lines.insert(0, i - 1)
|
||||
break
|
||||
if not delete_lines:
|
||||
raise CommandError(
|
||||
(
|
||||
"Couldn't find 'replaces =' line in file %s. "
|
||||
"Please finish cleaning up manually."
|
||||
)
|
||||
% (squashed_migration_fn,)
|
||||
)
|
||||
|
||||
if self.verbosity > 0 or self.interactive:
|
||||
self.stdout.write(
|
||||
self.style.MIGRATE_HEADING(
|
||||
"Will delete line %s%s from file %s"
|
||||
% (
|
||||
delete_lines[0],
|
||||
" and " + str(delete_lines[1]) if len(delete_lines) > 1 else "",
|
||||
squashed_migration_fn,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if not self.confirm():
|
||||
return
|
||||
|
||||
for line_num in sorted(delete_lines, reverse=True):
|
||||
del squashed_migration_lines[line_num]
|
||||
|
||||
with open(squashed_migration_fn, "w") as fp:
|
||||
if not self.dry_run:
|
||||
fp.write("".join(squashed_migration_lines))
|
||||
|
||||
def confirm(self):
|
||||
if self.interactive:
|
||||
answer = None
|
||||
while not answer or answer not in "yn":
|
||||
answer = input("Do you wish to proceed? [yN] ")
|
||||
if not answer:
|
||||
answer = "n"
|
||||
break
|
||||
else:
|
||||
answer = answer[0].lower()
|
||||
return answer == "y"
|
||||
return True
|
||||
|
||||
def find_migration(self, loader, app_label, name):
|
||||
try:
|
||||
return loader.get_migration_by_prefix(app_label, name)
|
||||
except AmbiguityError:
|
||||
raise CommandError(
|
||||
"More than one migration matches '%s' in app '%s'. Please be "
|
||||
"more specific." % (name, app_label)
|
||||
)
|
||||
except KeyError:
|
||||
raise CommandError(
|
||||
"Cannot find a migration matching '%s' from app '%s'."
|
||||
% (name, app_label)
|
||||
)
|
||||
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.apps import apps
|
||||
from django.core.management.base import CommandError, LabelCommand
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
|
||||
class Command(LabelCommand):
|
||||
help = "Outputs the specified model as a form definition to the shell."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("label", type=str, help="application name and model name")
|
||||
parser.add_argument(
|
||||
"--fields",
|
||||
"-f",
|
||||
action="append",
|
||||
dest="fields",
|
||||
default=[],
|
||||
help="Describe form with these fields only",
|
||||
)
|
||||
|
||||
@signalcommand
|
||||
def handle(self, *args, **options):
|
||||
label = options["label"]
|
||||
fields = options["fields"]
|
||||
|
||||
return describe_form(label, fields)
|
||||
|
||||
|
||||
def describe_form(label, fields):
|
||||
"""Return a string describing a form based on the model"""
|
||||
try:
|
||||
app_name, model_name = label.split(".")[-2:]
|
||||
except (IndexError, ValueError):
|
||||
raise CommandError("Need application and model name in the form: appname.model")
|
||||
model = apps.get_model(app_name, model_name)
|
||||
|
||||
opts = model._meta
|
||||
field_list = []
|
||||
for f in opts.fields + opts.many_to_many:
|
||||
if not f.editable:
|
||||
continue
|
||||
if fields and f.name not in fields:
|
||||
continue
|
||||
formfield = f.formfield()
|
||||
if "__dict__" not in dir(formfield):
|
||||
continue
|
||||
attrs = {}
|
||||
valid_fields = [
|
||||
"required",
|
||||
"initial",
|
||||
"max_length",
|
||||
"min_length",
|
||||
"max_value",
|
||||
"min_value",
|
||||
"max_digits",
|
||||
"decimal_places",
|
||||
"choices",
|
||||
"help_text",
|
||||
"label",
|
||||
]
|
||||
for k, v in formfield.__dict__.items():
|
||||
if k in valid_fields and v is not None:
|
||||
# ignore defaults, to minimize verbosity
|
||||
if k == "required" and v:
|
||||
continue
|
||||
if k == "help_text" and not v:
|
||||
continue
|
||||
if k == "widget":
|
||||
attrs[k] = v.__class__
|
||||
elif k in ["help_text", "label"]:
|
||||
attrs[k] = str(force_str(v).strip())
|
||||
else:
|
||||
attrs[k] = v
|
||||
|
||||
params = ", ".join(["%s=%r" % (k, v) for k, v in sorted(attrs.items())])
|
||||
field_list.append(
|
||||
" %(field_name)s = forms.%(field_type)s(%(params)s)"
|
||||
% {
|
||||
"field_name": f.name,
|
||||
"field_type": formfield.__class__.__name__,
|
||||
"params": params,
|
||||
}
|
||||
)
|
||||
return """
|
||||
from django import forms
|
||||
from %(app_name)s.models import %(object_name)s
|
||||
|
||||
class %(object_name)sForm(forms.Form):
|
||||
%(field_list)s
|
||||
""" % {
|
||||
"app_name": app_name,
|
||||
"object_name": opts.object_name,
|
||||
"field_list": "\n".join(field_list),
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import importlib.util
|
||||
from itertools import count
|
||||
import os
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import DEFAULT_DB_ALIAS
|
||||
from django.db.backends.base.creation import TEST_DATABASE_PREFIX
|
||||
|
||||
from django_extensions.settings import SQLITE_ENGINES, POSTGRESQL_ENGINES, MYSQL_ENGINES
|
||||
from django_extensions.management.mysql import parse_mysql_cnf
|
||||
from django_extensions.management.utils import signalcommand
|
||||
from django_extensions.utils.deprecation import RemovedInNextVersionWarning
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Drops test database for this project."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
super().add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--noinput",
|
||||
"--no-input",
|
||||
action="store_false",
|
||||
dest="interactive",
|
||||
default=True,
|
||||
help="Tells Django to NOT prompt the user for input of any kind.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-U",
|
||||
"--user",
|
||||
action="store",
|
||||
dest="user",
|
||||
default=None,
|
||||
help="Use another user for the database then defined in settings.py",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-P",
|
||||
"--password",
|
||||
action="store",
|
||||
dest="password",
|
||||
default=None,
|
||||
help="Use another password for the database then defined in settings.py",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-D",
|
||||
"--dbname",
|
||||
action="store",
|
||||
dest="dbname",
|
||||
default=None,
|
||||
help="Use another database name then defined in settings.py",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-R",
|
||||
"--router",
|
||||
action="store",
|
||||
dest="router",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
help="Use this router-database other then defined in settings.py",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
help=(
|
||||
"Nominates a database to run command for. "
|
||||
'Defaults to the "%s" database.'
|
||||
)
|
||||
% DEFAULT_DB_ALIAS,
|
||||
)
|
||||
|
||||
@signalcommand
|
||||
def handle(self, *args, **options):
|
||||
"""Drop test database for this project."""
|
||||
database = options["database"]
|
||||
if options["router"] != DEFAULT_DB_ALIAS:
|
||||
warnings.warn(
|
||||
"--router is deprecated. You should use --database.",
|
||||
RemovedInNextVersionWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
database = options["router"]
|
||||
|
||||
dbinfo = settings.DATABASES.get(database)
|
||||
if dbinfo is None:
|
||||
raise CommandError("Unknown database %s" % database)
|
||||
|
||||
engine = dbinfo.get("ENGINE")
|
||||
|
||||
user = password = database_name = database_host = database_port = ""
|
||||
if engine == "mysql":
|
||||
(user, password, database_name, database_host, database_port) = (
|
||||
parse_mysql_cnf(dbinfo)
|
||||
)
|
||||
|
||||
user = options["user"] or dbinfo.get("USER") or user
|
||||
password = options["password"] or dbinfo.get("PASSWORD") or password
|
||||
|
||||
try:
|
||||
database_name = dbinfo["TEST"]["NAME"]
|
||||
except KeyError:
|
||||
database_name = None
|
||||
|
||||
if database_name is None:
|
||||
database_name = TEST_DATABASE_PREFIX + (
|
||||
options["dbname"] or dbinfo.get("NAME")
|
||||
)
|
||||
|
||||
if database_name is None or database_name == "":
|
||||
raise CommandError(
|
||||
"You need to specify DATABASE_NAME in your Django settings file."
|
||||
)
|
||||
|
||||
database_host = dbinfo.get("HOST") or database_host
|
||||
database_port = dbinfo.get("PORT") or database_port
|
||||
|
||||
verbosity = options["verbosity"]
|
||||
if options["interactive"]:
|
||||
confirm = input(
|
||||
"""
|
||||
You have requested to drop all test databases.
|
||||
This will IRREVERSIBLY DESTROY
|
||||
ALL data in the database "{db_name}"
|
||||
and all cloned test databases generated via
|
||||
the "--parallel" flag (these are sequentially
|
||||
named "{db_name}_1", "{db_name}_2", etc.).
|
||||
Are you sure you want to do this?
|
||||
|
||||
Type 'yes' to continue, or 'no' to cancel: """.format(db_name=database_name)
|
||||
)
|
||||
else:
|
||||
confirm = "yes"
|
||||
|
||||
if confirm != "yes":
|
||||
print("Reset cancelled.")
|
||||
return
|
||||
|
||||
def get_database_names(formatter):
|
||||
"""
|
||||
Return a generator of all possible test database names.
|
||||
e.g., 'test_foo', 'test_foo_1', test_foo_2', etc.
|
||||
|
||||
formatter: func returning a clone db name given the primary db name
|
||||
and the clone's number, e.g., 'test_foo_1' for mysql/postgres, and
|
||||
'test_foo_1..sqlite3' for sqlite (re: double dots, see comments).
|
||||
"""
|
||||
yield database_name
|
||||
yield from (formatter(database_name, n) for n in count(1))
|
||||
|
||||
if engine in SQLITE_ENGINES:
|
||||
# By default all sqlite test databases are created in memory.
|
||||
# There will only be database files to delete if the developer has
|
||||
# specified a test database name, which forces files to be written
|
||||
# to disk.
|
||||
|
||||
logging.info("Unlinking %s databases" % engine)
|
||||
|
||||
def format_filename(name, number):
|
||||
filename, ext = os.path.splitext(name)
|
||||
# Since splitext() includes the dot in 'ext', the inclusion of
|
||||
# the dot in the format string below is incorrect and creates a
|
||||
# double dot. Django makes this mistake, so it must be
|
||||
# replicated here. If fixed in Django, this code should be
|
||||
# updated accordingly.
|
||||
# Reference: https://code.djangoproject.com/ticket/32582
|
||||
return "{}_{}.{}".format(filename, number, ext)
|
||||
|
||||
try:
|
||||
for db_name in get_database_names(format_filename):
|
||||
if not os.path.isfile(db_name):
|
||||
break
|
||||
logging.info('Unlinking database named "%s"' % db_name)
|
||||
os.unlink(db_name)
|
||||
except OSError:
|
||||
return
|
||||
|
||||
elif engine in MYSQL_ENGINES:
|
||||
import MySQLdb as Database
|
||||
|
||||
kwargs = {
|
||||
"user": user,
|
||||
"passwd": password,
|
||||
}
|
||||
if database_host.startswith("/"):
|
||||
kwargs["unix_socket"] = database_host
|
||||
else:
|
||||
kwargs["host"] = database_host
|
||||
|
||||
if database_port:
|
||||
kwargs["port"] = int(database_port)
|
||||
|
||||
connection = Database.connect(**kwargs)
|
||||
cursor = connection.cursor()
|
||||
|
||||
for db_name in get_database_names("{}_{}".format):
|
||||
exists_query = (
|
||||
"SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA "
|
||||
"WHERE SCHEMA_NAME='%s';" % db_name
|
||||
)
|
||||
row_count = cursor.execute(exists_query)
|
||||
if row_count < 1:
|
||||
break
|
||||
drop_query = "DROP DATABASE IF EXISTS `%s`" % db_name
|
||||
logging.info('Executing: "' + drop_query + '"')
|
||||
cursor.execute(drop_query)
|
||||
|
||||
elif engine in POSTGRESQL_ENGINES:
|
||||
has_psycopg3 = importlib.util.find_spec("psycopg")
|
||||
if has_psycopg3:
|
||||
import psycopg as Database # NOQA
|
||||
else:
|
||||
import psycopg2 as Database # NOQA
|
||||
|
||||
conn_params = {"dbname": "template1"}
|
||||
if user:
|
||||
conn_params["user"] = user
|
||||
if password:
|
||||
conn_params["password"] = password
|
||||
if database_host:
|
||||
conn_params["host"] = database_host
|
||||
if database_port:
|
||||
conn_params["port"] = database_port
|
||||
|
||||
connection = Database.connect(**conn_params)
|
||||
if has_psycopg3:
|
||||
connection.autocommit = True
|
||||
else:
|
||||
connection.set_isolation_level(0) # autocommit false
|
||||
cursor = connection.cursor()
|
||||
|
||||
for db_name in get_database_names("{}_{}".format):
|
||||
exists_query = (
|
||||
"SELECT datname FROM pg_catalog.pg_database WHERE datname='%s';"
|
||||
% db_name
|
||||
)
|
||||
try:
|
||||
cursor.execute(exists_query)
|
||||
# NOTE: Unlike MySQLdb, the psycopg2 cursor does not return the row
|
||||
# count however both cursors provide it as a property
|
||||
if cursor.rowcount < 1:
|
||||
break
|
||||
drop_query = 'DROP DATABASE IF EXISTS "%s";' % db_name
|
||||
logging.info('Executing: "' + drop_query + '"')
|
||||
cursor.execute(drop_query)
|
||||
except Database.ProgrammingError as e:
|
||||
logging.exception("Error: %s" % str(e))
|
||||
return
|
||||
else:
|
||||
raise CommandError("Unknown database engine %s" % engine)
|
||||
|
||||
if verbosity >= 2 or options["interactive"]:
|
||||
print("Reset successful.")
|
||||
@@ -0,0 +1,855 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Title: Dumpscript management command
|
||||
Project: Hardytools (queryset-refactor version)
|
||||
Author: Will Hardy
|
||||
Date: June 2008
|
||||
Usage: python manage.py dumpscript appname > scripts/scriptname.py
|
||||
$Revision: 217 $
|
||||
|
||||
Description:
|
||||
Generates a Python script that will repopulate the database using objects.
|
||||
The advantage of this approach is that it is easy to understand, and more
|
||||
flexible than directly populating the database, or using XML.
|
||||
|
||||
* It also allows for new defaults to take effect and only transfers what is
|
||||
needed.
|
||||
* If a new database schema has a NEW ATTRIBUTE, it is simply not
|
||||
populated (using a default value will make the transition smooth :)
|
||||
* If a new database schema REMOVES AN ATTRIBUTE, it is simply ignored
|
||||
and the data moves across safely (I'm assuming we don't want this
|
||||
attribute anymore.
|
||||
* Problems may only occur if there is a new model and is now a required
|
||||
ForeignKey for an existing model. But this is easy to fix by editing the
|
||||
populate script. Half of the job is already done as all ForeingKey
|
||||
lookups occur though the locate_object() function in the generated script.
|
||||
|
||||
Improvements:
|
||||
See TODOs and FIXMEs scattered throughout :-)
|
||||
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import sys
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import router
|
||||
from django.db.models import (
|
||||
AutoField,
|
||||
BooleanField,
|
||||
DateField,
|
||||
DateTimeField,
|
||||
FileField,
|
||||
ForeignKey,
|
||||
)
|
||||
from django.db.models.deletion import Collector
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import force_str, smart_str
|
||||
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
|
||||
def orm_item_locator(orm_obj):
|
||||
"""
|
||||
Is called every time an object that will not be exported is required.
|
||||
Where orm_obj is the referred object.
|
||||
We postpone the lookup to locate_object() which will be run on the generated script
|
||||
"""
|
||||
|
||||
the_class = orm_obj._meta.object_name
|
||||
original_class = the_class
|
||||
pk_name = orm_obj._meta.pk.name
|
||||
original_pk_name = pk_name
|
||||
pk_value = getattr(orm_obj, pk_name)
|
||||
|
||||
while (
|
||||
hasattr(pk_value, "_meta")
|
||||
and hasattr(pk_value._meta, "pk")
|
||||
and hasattr(pk_value._meta.pk, "name")
|
||||
):
|
||||
the_class = pk_value._meta.object_name
|
||||
pk_name = pk_value._meta.pk.name
|
||||
pk_value = getattr(pk_value, pk_name)
|
||||
|
||||
clean_dict = make_clean_dict(orm_obj.__dict__)
|
||||
|
||||
for key in clean_dict:
|
||||
v = clean_dict[key]
|
||||
if v is not None:
|
||||
if isinstance(v, datetime.datetime):
|
||||
if not timezone.is_aware(v):
|
||||
v = timezone.make_aware(v)
|
||||
clean_dict[key] = StrToCodeChanger(
|
||||
'dateutil.parser.parse("%s")' % v.isoformat()
|
||||
)
|
||||
elif not isinstance(v, (str, int, float)):
|
||||
clean_dict[key] = str("%s" % v)
|
||||
|
||||
output = """ importer.locate_object(%s, "%s", %s, "%s", %s, %s ) """ % (
|
||||
original_class,
|
||||
original_pk_name,
|
||||
the_class,
|
||||
pk_name,
|
||||
pk_value,
|
||||
clean_dict,
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Dumps the data as a customised python script."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
super().add_arguments(parser)
|
||||
parser.add_argument("appname", nargs="+")
|
||||
parser.add_argument(
|
||||
"--autofield",
|
||||
action="store_false",
|
||||
dest="skip_autofield",
|
||||
default=True,
|
||||
help="Include Autofields (like pk fields)",
|
||||
)
|
||||
|
||||
@signalcommand
|
||||
def handle(self, *args, **options):
|
||||
app_labels = options["appname"]
|
||||
|
||||
# Get the models we want to export
|
||||
models = get_models(app_labels)
|
||||
|
||||
# A dictionary is created to keep track of all the processed objects,
|
||||
# so that foreign key references can be made using python variable names.
|
||||
# This variable "context" will be passed around like the town bicycle.
|
||||
context = {}
|
||||
|
||||
# Create a dumpscript object and let it format itself as a string
|
||||
script = Script(
|
||||
models=models,
|
||||
context=context,
|
||||
stdout=self.stdout,
|
||||
stderr=self.stderr,
|
||||
options=options,
|
||||
)
|
||||
self.stdout.write(str(script))
|
||||
self.stdout.write("\n")
|
||||
|
||||
|
||||
def get_models(app_labels):
|
||||
"""
|
||||
Get a list of models for the given app labels, with some exceptions.
|
||||
TODO: If a required model is referenced, it should also be included.
|
||||
Or at least discovered with a get_or_create() call.
|
||||
"""
|
||||
|
||||
# These models are not to be outputted,
|
||||
# e.g. because they can be generated automatically
|
||||
# TODO: This should be "appname.modelname" string
|
||||
EXCLUDED_MODELS = (ContentType,)
|
||||
|
||||
models = []
|
||||
|
||||
# If no app labels are given, return all
|
||||
if not app_labels:
|
||||
for app in apps.get_app_configs():
|
||||
models += [
|
||||
m
|
||||
for m in apps.get_app_config(app.label).get_models()
|
||||
if m not in EXCLUDED_MODELS
|
||||
]
|
||||
return models
|
||||
|
||||
# Get all relevant apps
|
||||
for app_label in app_labels:
|
||||
# If a specific model is mentioned, get only that model
|
||||
if "." in app_label:
|
||||
app_label, model_name = app_label.split(".", 1)
|
||||
models.append(apps.get_model(app_label, model_name))
|
||||
# Get all models for a given app
|
||||
else:
|
||||
models += [
|
||||
m
|
||||
for m in apps.get_app_config(app_label).get_models()
|
||||
if m not in EXCLUDED_MODELS
|
||||
]
|
||||
|
||||
return models
|
||||
|
||||
|
||||
class Code:
|
||||
"""
|
||||
A snippet of python script.
|
||||
This keeps track of import statements and can be output to a string.
|
||||
In the future, other features such as custom indentation might be included
|
||||
in this class.
|
||||
"""
|
||||
|
||||
def __init__(self, indent=-1, stdout=None, stderr=None):
|
||||
if not stdout:
|
||||
stdout = sys.stdout
|
||||
if not stderr:
|
||||
stderr = sys.stderr
|
||||
|
||||
self.indent = indent
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of this script."""
|
||||
if self.imports:
|
||||
self.stderr.write(repr(self.import_lines))
|
||||
return flatten_blocks(
|
||||
[""] + self.import_lines + [""] + self.lines, num_indents=self.indent
|
||||
)
|
||||
else:
|
||||
return flatten_blocks(self.lines, num_indents=self.indent)
|
||||
|
||||
def get_import_lines(self):
|
||||
"""Take the stored imports and converts them to lines"""
|
||||
if self.imports:
|
||||
return [
|
||||
"from %s import %s" % (value, key)
|
||||
for key, value in self.imports.items()
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
import_lines = property(get_import_lines)
|
||||
|
||||
|
||||
class ModelCode(Code):
|
||||
"""Produces a python script that can recreate data for a given model class."""
|
||||
|
||||
def __init__(self, model, context=None, stdout=None, stderr=None, options=None):
|
||||
super().__init__(indent=0, stdout=stdout, stderr=stderr)
|
||||
self.model = model
|
||||
if context is None:
|
||||
context = {}
|
||||
self.context = context
|
||||
self.options = options
|
||||
self.instances = []
|
||||
|
||||
def get_imports(self):
|
||||
"""
|
||||
Return a dictionary of import statements, with the variable being
|
||||
defined as the key.
|
||||
"""
|
||||
return {self.model.__name__: smart_str(self.model.__module__)}
|
||||
|
||||
imports = property(get_imports)
|
||||
|
||||
def get_lines(self):
|
||||
"""
|
||||
Return a list of lists or strings, representing the code body.
|
||||
Each list is a block, each string is a statement.
|
||||
"""
|
||||
code = []
|
||||
|
||||
for counter, item in enumerate(self.model._default_manager.all()):
|
||||
instance = InstanceCode(
|
||||
instance=item,
|
||||
id=counter + 1,
|
||||
context=self.context,
|
||||
stdout=self.stdout,
|
||||
stderr=self.stderr,
|
||||
options=self.options,
|
||||
)
|
||||
self.instances.append(instance)
|
||||
if instance.waiting_list:
|
||||
code += instance.lines
|
||||
|
||||
# After each instance has been processed, try again.
|
||||
# This allows self referencing fields to work.
|
||||
for instance in self.instances:
|
||||
if instance.waiting_list:
|
||||
code += instance.lines
|
||||
|
||||
return code
|
||||
|
||||
lines = property(get_lines)
|
||||
|
||||
|
||||
class InstanceCode(Code):
|
||||
"""Produces a python script that can recreate data for a given model instance."""
|
||||
|
||||
def __init__(
|
||||
self, instance, id, context=None, stdout=None, stderr=None, options=None
|
||||
):
|
||||
"""We need the instance in question and an id"""
|
||||
|
||||
super().__init__(indent=0, stdout=stdout, stderr=stderr)
|
||||
self.imports = {}
|
||||
|
||||
self.options = options
|
||||
self.instance = instance
|
||||
self.model = self.instance.__class__
|
||||
if context is None:
|
||||
context = {}
|
||||
self.context = context
|
||||
self.variable_name = "%s_%s" % (self.instance._meta.db_table, id)
|
||||
self.skip_me = None
|
||||
self.instantiated = False
|
||||
|
||||
self.waiting_list = list(self.model._meta.fields)
|
||||
|
||||
self.many_to_many_waiting_list = {}
|
||||
for field in self.model._meta.many_to_many:
|
||||
try:
|
||||
if not field.remote_field.through._meta.auto_created:
|
||||
continue
|
||||
except AttributeError:
|
||||
pass
|
||||
self.many_to_many_waiting_list[field] = list(
|
||||
getattr(self.instance, field.name).all()
|
||||
)
|
||||
|
||||
def get_lines(self, force=False):
|
||||
"""
|
||||
Return a list of lists or strings, representing the code body.
|
||||
Each list is a block, each string is a statement.
|
||||
|
||||
force (True or False): if an attribute object cannot be included,
|
||||
it is usually skipped to be processed later. With 'force' set, there
|
||||
will be no waiting: a get_or_create() call is written instead.
|
||||
"""
|
||||
code_lines = []
|
||||
|
||||
# Don't return anything if this is an instance that should be skipped
|
||||
if self.skip():
|
||||
return []
|
||||
|
||||
# Initialise our new object
|
||||
# e.g. model_name_35 = Model()
|
||||
code_lines += self.instantiate()
|
||||
|
||||
# Add each field
|
||||
# e.g. model_name_35.field_one = 1034.91
|
||||
# model_name_35.field_two = "text"
|
||||
code_lines += self.get_waiting_list()
|
||||
|
||||
if force:
|
||||
# TODO: Check that M2M are not affected
|
||||
code_lines += self.get_waiting_list(force=force)
|
||||
|
||||
# Print the save command for our new object
|
||||
# e.g. model_name_35.save()
|
||||
if code_lines:
|
||||
code_lines.append(
|
||||
"%s = importer.save_or_locate(%s)\n"
|
||||
% (self.variable_name, self.variable_name)
|
||||
)
|
||||
|
||||
code_lines += self.get_many_to_many_lines(force=force)
|
||||
|
||||
return code_lines
|
||||
|
||||
lines = property(get_lines)
|
||||
|
||||
def skip(self):
|
||||
"""
|
||||
Determine whether or not this object should be skipped.
|
||||
If this model instance is a parent of a single subclassed
|
||||
instance, skip it. The subclassed instance will create this
|
||||
parent instance for us.
|
||||
|
||||
TODO: Allow the user to force its creation?
|
||||
"""
|
||||
if self.skip_me is not None:
|
||||
return self.skip_me
|
||||
|
||||
cls = self.instance.__class__
|
||||
using = router.db_for_write(cls, instance=self.instance)
|
||||
collector = Collector(using=using)
|
||||
collector.collect([self.instance], collect_related=False)
|
||||
sub_objects = sum([list(i) for i in collector.data.values()], [])
|
||||
sub_objects_parents = [so._meta.parents for so in sub_objects]
|
||||
if [self.model in p for p in sub_objects_parents].count(True) == 1:
|
||||
# since this instance isn't explicitly created, it's variable name
|
||||
# can't be referenced in the script, so record None in context dict
|
||||
pk_name = self.instance._meta.pk.name
|
||||
key = "%s_%s" % (self.model.__name__, getattr(self.instance, pk_name))
|
||||
self.context[key] = None
|
||||
self.skip_me = True
|
||||
else:
|
||||
self.skip_me = False
|
||||
|
||||
return self.skip_me
|
||||
|
||||
def instantiate(self):
|
||||
"""Write lines for instantiation"""
|
||||
# e.g. model_name_35 = Model()
|
||||
code_lines = []
|
||||
|
||||
if not self.instantiated:
|
||||
code_lines.append("%s = %s()" % (self.variable_name, self.model.__name__))
|
||||
self.instantiated = True
|
||||
|
||||
# Store our variable name for future foreign key references
|
||||
pk_name = self.instance._meta.pk.name
|
||||
key = "%s_%s" % (self.model.__name__, getattr(self.instance, pk_name))
|
||||
self.context[key] = self.variable_name
|
||||
|
||||
return code_lines
|
||||
|
||||
def get_waiting_list(self, force=False):
|
||||
"""Add lines for any waiting fields that can be completed now."""
|
||||
|
||||
code_lines = []
|
||||
skip_autofield = self.options["skip_autofield"]
|
||||
|
||||
# Process normal fields
|
||||
for field in list(self.waiting_list):
|
||||
try:
|
||||
# Find the value, add the line, remove from waiting list and move on
|
||||
value = get_attribute_value(
|
||||
self.instance,
|
||||
field,
|
||||
self.context,
|
||||
force=force,
|
||||
skip_autofield=skip_autofield,
|
||||
)
|
||||
code_lines.append(
|
||||
"%s.%s = %s" % (self.variable_name, field.name, value)
|
||||
)
|
||||
self.waiting_list.remove(field)
|
||||
except SkipValue:
|
||||
# Remove from the waiting list and move on
|
||||
self.waiting_list.remove(field)
|
||||
continue
|
||||
except DoLater:
|
||||
# Move on, maybe next time
|
||||
continue
|
||||
|
||||
return code_lines
|
||||
|
||||
def get_many_to_many_lines(self, force=False):
|
||||
"""Generate lines that define many to many relations for this instance."""
|
||||
|
||||
lines = []
|
||||
|
||||
for field, rel_items in self.many_to_many_waiting_list.items():
|
||||
for rel_item in list(rel_items):
|
||||
try:
|
||||
pk_name = rel_item._meta.pk.name
|
||||
key = "%s_%s" % (
|
||||
rel_item.__class__.__name__,
|
||||
getattr(rel_item, pk_name),
|
||||
)
|
||||
value = "%s" % self.context[key]
|
||||
lines.append(
|
||||
"%s.%s.add(%s)" % (self.variable_name, field.name, value)
|
||||
)
|
||||
self.many_to_many_waiting_list[field].remove(rel_item)
|
||||
except KeyError:
|
||||
if force:
|
||||
item_locator = orm_item_locator(rel_item)
|
||||
self.context["__extra_imports"][rel_item._meta.object_name] = (
|
||||
rel_item.__module__
|
||||
)
|
||||
lines.append(
|
||||
"%s.%s.add( %s )"
|
||||
% (self.variable_name, field.name, item_locator)
|
||||
)
|
||||
self.many_to_many_waiting_list[field].remove(rel_item)
|
||||
|
||||
if lines:
|
||||
lines.append("")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
class Script(Code):
|
||||
"""Produces a complete python script that can recreate data for the given apps."""
|
||||
|
||||
def __init__(self, models, context=None, stdout=None, stderr=None, options=None):
|
||||
super().__init__(stdout=stdout, stderr=stderr)
|
||||
self.imports = {}
|
||||
|
||||
self.models = models
|
||||
if context is None:
|
||||
context = {}
|
||||
self.context = context
|
||||
|
||||
self.context["__avaliable_models"] = set(models)
|
||||
self.context["__extra_imports"] = {}
|
||||
|
||||
self.options = options
|
||||
|
||||
def _queue_models(self, models, context):
|
||||
"""
|
||||
Work an an appropriate ordering for the models.
|
||||
This isn't essential, but makes the script look nicer because
|
||||
more instances can be defined on their first try.
|
||||
"""
|
||||
model_queue = []
|
||||
number_remaining_models = len(models)
|
||||
# Max number of cycles allowed before we call it an infinite loop.
|
||||
MAX_CYCLES = number_remaining_models
|
||||
allowed_cycles = MAX_CYCLES
|
||||
|
||||
while number_remaining_models > 0:
|
||||
previous_number_remaining_models = number_remaining_models
|
||||
|
||||
model = models.pop(0)
|
||||
|
||||
# If the model is ready to be processed, add it to the list
|
||||
if check_dependencies(model, model_queue, context["__avaliable_models"]):
|
||||
model_class = ModelCode(
|
||||
model=model,
|
||||
context=context,
|
||||
stdout=self.stdout,
|
||||
stderr=self.stderr,
|
||||
options=self.options,
|
||||
)
|
||||
model_queue.append(model_class)
|
||||
|
||||
# Otherwise put the model back at the end of the list
|
||||
else:
|
||||
models.append(model)
|
||||
|
||||
# Check for infinite loops.
|
||||
# This means there is a cyclic foreign key structure
|
||||
# That cannot be resolved by re-ordering
|
||||
number_remaining_models = len(models)
|
||||
if number_remaining_models == previous_number_remaining_models:
|
||||
allowed_cycles -= 1
|
||||
if allowed_cycles <= 0:
|
||||
# Add remaining models, but do not remove them from the model list
|
||||
missing_models = [
|
||||
ModelCode(
|
||||
model=m,
|
||||
context=context,
|
||||
stdout=self.stdout,
|
||||
stderr=self.stderr,
|
||||
options=self.options,
|
||||
)
|
||||
for m in models
|
||||
]
|
||||
model_queue += missing_models
|
||||
# Replace the models with the model class objects
|
||||
# (sure, this is a little bit of hackery)
|
||||
models[:] = missing_models
|
||||
break
|
||||
else:
|
||||
allowed_cycles = MAX_CYCLES
|
||||
|
||||
return model_queue
|
||||
|
||||
def get_lines(self):
|
||||
"""
|
||||
Return a list of lists or strings, representing the code body.
|
||||
Each list is a block, each string is a statement.
|
||||
"""
|
||||
code = [self.FILE_HEADER.strip()]
|
||||
|
||||
# Queue and process the required models
|
||||
for model_class in self._queue_models(self.models, context=self.context):
|
||||
msg = "Processing model: %s.%s\n" % (
|
||||
model_class.model.__module__,
|
||||
model_class.model.__name__,
|
||||
)
|
||||
self.stderr.write(msg)
|
||||
code.append(" # " + msg)
|
||||
code.append(model_class.import_lines)
|
||||
code.append("")
|
||||
code.append(model_class.lines)
|
||||
|
||||
# Process left over foreign keys from cyclic models
|
||||
for model in self.models:
|
||||
msg = "Re-processing model: %s.%s\n" % (
|
||||
model.model.__module__,
|
||||
model.model.__name__,
|
||||
)
|
||||
self.stderr.write(msg)
|
||||
code.append(" # " + msg)
|
||||
for instance in model.instances:
|
||||
if instance.waiting_list or instance.many_to_many_waiting_list:
|
||||
code.append(instance.get_lines(force=True))
|
||||
|
||||
code.insert(1, " # Initial Imports")
|
||||
code.insert(2, "")
|
||||
for key, value in self.context["__extra_imports"].items():
|
||||
code.insert(2, " from %s import %s" % (value, key))
|
||||
|
||||
return code
|
||||
|
||||
lines = property(get_lines)
|
||||
|
||||
# A user-friendly file header
|
||||
FILE_HEADER = """
|
||||
|
||||
#!/usr/bin/env python
|
||||
|
||||
|
||||
# This file has been automatically generated.
|
||||
# Instead of changing it, create a file called import_helper.py
|
||||
# and put there a class called ImportHelper(object) in it.
|
||||
#
|
||||
# This class will be specially cast so that instead of extending object,
|
||||
# it will actually extend the class BasicImportHelper()
|
||||
#
|
||||
# That means you just have to overload the methods you want to
|
||||
# change, leaving the other ones intact.
|
||||
#
|
||||
# Something that you might want to do is use transactions, for example.
|
||||
#
|
||||
# Also, don't forget to add the necessary Django imports.
|
||||
#
|
||||
# This file was generated with the following command:
|
||||
# %s
|
||||
#
|
||||
# to restore it, run
|
||||
# manage.py runscript module_name.this_script_name
|
||||
#
|
||||
# example: if manage.py is at ./manage.py
|
||||
# and the script is at ./some_folder/some_script.py
|
||||
# you must make sure ./some_folder/__init__.py exists
|
||||
# and run ./manage.py runscript some_folder.some_script
|
||||
import os, sys
|
||||
from django.db import transaction
|
||||
|
||||
class BasicImportHelper:
|
||||
|
||||
def pre_import(self):
|
||||
pass
|
||||
|
||||
@transaction.atomic
|
||||
def run_import(self, import_data):
|
||||
import_data()
|
||||
|
||||
def post_import(self):
|
||||
pass
|
||||
|
||||
def locate_similar(self, current_object, search_data):
|
||||
# You will probably want to call this method from save_or_locate()
|
||||
# Example:
|
||||
# new_obj = self.locate_similar(the_obj, {"national_id": the_obj.national_id } )
|
||||
|
||||
the_obj = current_object.__class__.objects.get(**search_data)
|
||||
return the_obj
|
||||
|
||||
def locate_object(self, original_class, original_pk_name, the_class, pk_name, pk_value, obj_content):
|
||||
# You may change this function to do specific lookup for specific objects
|
||||
#
|
||||
# original_class class of the django orm's object that needs to be located
|
||||
# original_pk_name the primary key of original_class
|
||||
# the_class parent class of original_class which contains obj_content
|
||||
# pk_name the primary key of original_class
|
||||
# pk_value value of the primary_key
|
||||
# obj_content content of the object which was not exported.
|
||||
#
|
||||
# You should use obj_content to locate the object on the target db
|
||||
#
|
||||
# An example where original_class and the_class are different is
|
||||
# when original_class is Farmer and the_class is Person. The table
|
||||
# may refer to a Farmer but you will actually need to locate Person
|
||||
# in order to instantiate that Farmer
|
||||
#
|
||||
# Example:
|
||||
# if the_class == SurveyResultFormat or the_class == SurveyType or the_class == SurveyState:
|
||||
# pk_name="name"
|
||||
# pk_value=obj_content[pk_name]
|
||||
# if the_class == StaffGroup:
|
||||
# pk_value=8
|
||||
|
||||
search_data = { pk_name: pk_value }
|
||||
the_obj = the_class.objects.get(**search_data)
|
||||
#print(the_obj)
|
||||
return the_obj
|
||||
|
||||
|
||||
def save_or_locate(self, the_obj):
|
||||
# Change this if you want to locate the object in the database
|
||||
try:
|
||||
the_obj.save()
|
||||
except:
|
||||
print("---------------")
|
||||
print("Error saving the following object:")
|
||||
print(the_obj.__class__)
|
||||
print(" ")
|
||||
print(the_obj.__dict__)
|
||||
print(" ")
|
||||
print(the_obj)
|
||||
print(" ")
|
||||
print("---------------")
|
||||
|
||||
raise
|
||||
return the_obj
|
||||
|
||||
|
||||
importer = None
|
||||
try:
|
||||
import import_helper
|
||||
# We need this so ImportHelper can extend BasicImportHelper, although import_helper.py
|
||||
# has no knowlodge of this class
|
||||
importer = type("DynamicImportHelper", (import_helper.ImportHelper, BasicImportHelper ) , {} )()
|
||||
except ImportError as e:
|
||||
# From Python 3.3 we can check e.name - string match is for backward compatibility.
|
||||
if 'import_helper' in str(e):
|
||||
importer = BasicImportHelper()
|
||||
else:
|
||||
raise
|
||||
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
try:
|
||||
import dateutil.parser
|
||||
from dateutil.tz import tzoffset
|
||||
except ImportError:
|
||||
print("Please install python-dateutil")
|
||||
sys.exit(os.EX_USAGE)
|
||||
|
||||
def run():
|
||||
importer.pre_import()
|
||||
importer.run_import(import_data)
|
||||
importer.post_import()
|
||||
|
||||
def import_data():
|
||||
|
||||
""" % " ".join(sys.argv) # noqa: E501
|
||||
|
||||
|
||||
# HELPER FUNCTIONS
|
||||
# -------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def flatten_blocks(lines, num_indents=-1):
|
||||
"""
|
||||
Take a list (block) or string (statement) and flattens it into a string
|
||||
with indentation.
|
||||
"""
|
||||
# The standard indent is four spaces
|
||||
INDENTATION = " " * 4
|
||||
|
||||
if not lines:
|
||||
return ""
|
||||
|
||||
# If this is a string, add the indentation and finish here
|
||||
if isinstance(lines, str):
|
||||
return INDENTATION * num_indents + lines
|
||||
|
||||
# If this is not a string, join the lines and recurse
|
||||
return "\n".join([flatten_blocks(line, num_indents + 1) for line in lines])
|
||||
|
||||
|
||||
def get_attribute_value(item, field, context, force=False, skip_autofield=True):
|
||||
"""Get a string version of the given attribute's value, like repr() might."""
|
||||
# Find the value of the field, catching any database issues
|
||||
try:
|
||||
value = getattr(item, field.name)
|
||||
except ObjectDoesNotExist:
|
||||
raise SkipValue(
|
||||
"Could not find object for %s.%s, ignoring.\n"
|
||||
% (item.__class__.__name__, field.name)
|
||||
)
|
||||
|
||||
# AutoField: We don't include the auto fields, they'll be automatically recreated
|
||||
if skip_autofield and isinstance(field, AutoField):
|
||||
raise SkipValue()
|
||||
|
||||
# Some databases (eg MySQL) might store boolean values as 0/1,
|
||||
# this needs to be cast as a bool
|
||||
elif isinstance(field, BooleanField) and value is not None:
|
||||
return repr(bool(value))
|
||||
|
||||
# Post file-storage-refactor, repr() on File/ImageFields no longer returns the path
|
||||
elif isinstance(field, FileField):
|
||||
return repr(force_str(value))
|
||||
|
||||
# ForeignKey fields, link directly using our stored python variable name
|
||||
elif isinstance(field, ForeignKey) and value is not None:
|
||||
# Special case for contenttype foreign keys: no need to output any
|
||||
# content types in this script, as they can be generated again
|
||||
# automatically.
|
||||
# NB: Not sure if "is" will always work
|
||||
if field.remote_field.model is ContentType:
|
||||
return 'ContentType.objects.get(app_label="%s", model="%s")' % (
|
||||
value.app_label,
|
||||
value.model,
|
||||
)
|
||||
|
||||
# Generate an identifier (key) for this foreign object
|
||||
pk_name = value._meta.pk.name
|
||||
key = "%s_%s" % (value.__class__.__name__, getattr(value, pk_name))
|
||||
|
||||
if key in context:
|
||||
variable_name = context[key]
|
||||
# If the context value is set to None, this should be skipped.
|
||||
# This identifies models that have been skipped (inheritance)
|
||||
if variable_name is None:
|
||||
raise SkipValue()
|
||||
# Return the variable name listed in the context
|
||||
return "%s" % variable_name
|
||||
elif value.__class__ not in context["__avaliable_models"] or force:
|
||||
context["__extra_imports"][value._meta.object_name] = value.__module__
|
||||
item_locator = orm_item_locator(value)
|
||||
return item_locator
|
||||
else:
|
||||
raise DoLater("(FK) %s.%s\n" % (item.__class__.__name__, field.name))
|
||||
|
||||
elif isinstance(field, (DateField, DateTimeField)) and value is not None:
|
||||
return 'dateutil.parser.parse("%s")' % value.isoformat()
|
||||
|
||||
# A normal field (e.g. a python built-in)
|
||||
else:
|
||||
return repr(value)
|
||||
|
||||
|
||||
def make_clean_dict(the_dict):
|
||||
if "_state" in the_dict:
|
||||
clean_dict = the_dict.copy()
|
||||
del clean_dict["_state"]
|
||||
return clean_dict
|
||||
return the_dict
|
||||
|
||||
|
||||
def check_dependencies(model, model_queue, avaliable_models):
|
||||
"""Check that all the depenedencies for this model are already in the queue."""
|
||||
# A list of allowed links: existing fields, itself and the special case ContentType
|
||||
allowed_links = [m.model.__name__ for m in model_queue] + [
|
||||
model.__name__,
|
||||
"ContentType",
|
||||
]
|
||||
|
||||
# For each ForeignKey or ManyToMany field, check that a link is possible
|
||||
|
||||
for field in model._meta.fields:
|
||||
if not field.remote_field:
|
||||
continue
|
||||
if field.remote_field.model.__name__ not in allowed_links:
|
||||
if field.remote_field.model not in avaliable_models:
|
||||
continue
|
||||
return False
|
||||
|
||||
for field in model._meta.many_to_many:
|
||||
if not field.remote_field:
|
||||
continue
|
||||
if field.remote_field.model.__name__ not in allowed_links:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# EXCEPTIONS
|
||||
# -------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SkipValue(Exception):
|
||||
"""Value could not be parsed or should simply be skipped."""
|
||||
|
||||
|
||||
class DoLater(Exception):
|
||||
"""Value could not be parsed or should simply be skipped."""
|
||||
|
||||
|
||||
class StrToCodeChanger:
|
||||
def __init__(self, string):
|
||||
self.repr = string
|
||||
|
||||
def __repr__(self):
|
||||
return self.repr
|
||||
@@ -0,0 +1,212 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
import csv
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
|
||||
FORMATS = [
|
||||
"address",
|
||||
"emails",
|
||||
"google",
|
||||
"outlook",
|
||||
"linkedin",
|
||||
"vcard",
|
||||
]
|
||||
|
||||
|
||||
def full_name(**kwargs):
|
||||
"""Return full name or username."""
|
||||
first_name = kwargs.get("first_name")
|
||||
last_name = kwargs.get("last_name")
|
||||
|
||||
name = " ".join(n for n in [first_name, last_name] if n)
|
||||
if name:
|
||||
return name
|
||||
|
||||
name = kwargs.get("name")
|
||||
if name:
|
||||
return name
|
||||
|
||||
username = kwargs.get("username")
|
||||
if username:
|
||||
return username
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Export user email address list in one of a number of formats."
|
||||
args = "[output file]"
|
||||
label = "filename to save to"
|
||||
|
||||
can_import_settings = True
|
||||
encoding = "utf-8" # RED_FLAG: add as an option -DougN
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.UserModel = get_user_model()
|
||||
|
||||
def add_arguments(self, parser):
|
||||
super().add_arguments(parser)
|
||||
(
|
||||
parser.add_argument(
|
||||
"--group",
|
||||
"-g",
|
||||
action="store",
|
||||
dest="group",
|
||||
default=None,
|
||||
help="Limit to users which are part of the supplied group name",
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
"-f",
|
||||
action="store",
|
||||
dest="format",
|
||||
default=FORMATS[0],
|
||||
help="output format. May be one of %s." % ", ".join(FORMATS),
|
||||
)
|
||||
|
||||
def full_name(self, **kwargs):
|
||||
return getattr(settings, "EXPORT_EMAILS_FULL_NAME_FUNC", full_name)(**kwargs)
|
||||
|
||||
@signalcommand
|
||||
def handle(self, *args, **options):
|
||||
if len(args) > 1:
|
||||
raise CommandError("extra arguments supplied")
|
||||
group = options["group"]
|
||||
if group and not Group.objects.filter(name=group).count() == 1:
|
||||
names = "', '".join(g["name"] for g in Group.objects.values("name"))
|
||||
if names:
|
||||
names = "'" + names + "'."
|
||||
raise CommandError(
|
||||
"Unknown group '" + group + "'. Valid group names are: " + names
|
||||
)
|
||||
|
||||
UserModel = get_user_model()
|
||||
order_by = getattr(
|
||||
settings,
|
||||
"EXPORT_EMAILS_ORDER_BY",
|
||||
["last_name", "first_name", "username", "email"],
|
||||
)
|
||||
fields = getattr(
|
||||
settings,
|
||||
"EXPORT_EMAILS_FIELDS",
|
||||
["last_name", "first_name", "username", "email"],
|
||||
)
|
||||
|
||||
qs = UserModel.objects.all().order_by(*order_by)
|
||||
if group:
|
||||
qs = qs.filter(groups__name=group).distinct()
|
||||
qs = qs.values(*fields)
|
||||
getattr(self, options["format"])(qs)
|
||||
|
||||
def address(self, qs):
|
||||
"""
|
||||
Single entry per line in the format of:
|
||||
"full name" <my@address.com>;
|
||||
"""
|
||||
self.stdout.write(
|
||||
"\n".join(
|
||||
'"%s" <%s>;' % (self.full_name(**ent), ent.get("email", ""))
|
||||
for ent in qs
|
||||
)
|
||||
)
|
||||
self.stdout.write("\n")
|
||||
|
||||
def emails(self, qs):
|
||||
"""
|
||||
Single entry with email only in the format of:
|
||||
my@address.com,
|
||||
"""
|
||||
self.stdout.write(",\n".join(ent["email"] for ent in qs if ent.get("email")))
|
||||
self.stdout.write("\n")
|
||||
|
||||
def google(self, qs):
|
||||
"""CSV format suitable for importing into google GMail"""
|
||||
csvf = csv.writer(sys.stdout)
|
||||
csvf.writerow(["Name", "Email"])
|
||||
for ent in qs:
|
||||
csvf.writerow([self.full_name(**ent), ent.get("email", "")])
|
||||
|
||||
def linkedin(self, qs):
|
||||
"""
|
||||
CSV format suitable for importing into linkedin Groups.
|
||||
perfect for pre-approving members of a linkedin group.
|
||||
"""
|
||||
csvf = csv.writer(sys.stdout)
|
||||
csvf.writerow(["First Name", "Last Name", "Email"])
|
||||
for ent in qs:
|
||||
csvf.writerow(
|
||||
[
|
||||
ent.get("first_name", ""),
|
||||
ent.get("last_name", ""),
|
||||
ent.get("email", ""),
|
||||
]
|
||||
)
|
||||
|
||||
def outlook(self, qs):
|
||||
"""CSV format suitable for importing into outlook"""
|
||||
csvf = csv.writer(sys.stdout)
|
||||
columns = [
|
||||
"Name",
|
||||
"E-mail Address",
|
||||
"Notes",
|
||||
"E-mail 2 Address",
|
||||
"E-mail 3 Address",
|
||||
"Mobile Phone",
|
||||
"Pager",
|
||||
"Company",
|
||||
"Job Title",
|
||||
"Home Phone",
|
||||
"Home Phone 2",
|
||||
"Home Fax",
|
||||
"Home Address",
|
||||
"Business Phone",
|
||||
"Business Phone 2",
|
||||
"Business Fax",
|
||||
"Business Address",
|
||||
"Other Phone",
|
||||
"Other Fax",
|
||||
"Other Address",
|
||||
]
|
||||
csvf.writerow(columns)
|
||||
empty = [""] * (len(columns) - 2)
|
||||
for ent in qs:
|
||||
csvf.writerow([self.full_name(**ent), ent.get("email", "")] + empty)
|
||||
|
||||
def vcard(self, qs):
|
||||
"""VCARD format."""
|
||||
try:
|
||||
import vobject
|
||||
except ImportError:
|
||||
print(
|
||||
self.style.ERROR(
|
||||
"Please install vobject to use the vcard export format."
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
out = sys.stdout
|
||||
for ent in qs:
|
||||
card = vobject.vCard()
|
||||
card.add("fn").value = self.full_name(**ent)
|
||||
if ent.get("last_name") and ent.get("first_name"):
|
||||
card.add("n").value = vobject.vcard.Name(
|
||||
ent["last_name"], ent["first_name"]
|
||||
)
|
||||
else:
|
||||
# fallback to fullname, if both first and lastname are not declared
|
||||
card.add("n").value = vobject.vcard.Name(self.full_name(**ent))
|
||||
if ent.get("email"):
|
||||
emailpart = card.add("email")
|
||||
emailpart.value = ent["email"]
|
||||
emailpart.type_param = "INTERNET"
|
||||
|
||||
out.write(card.serialize())
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
|
||||
from django.core.management.base import LabelCommand
|
||||
from django.template import TemplateDoesNotExist, loader
|
||||
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
|
||||
class Command(LabelCommand):
|
||||
help = "Finds the location of the given template by resolving its path"
|
||||
args = "[template_path]"
|
||||
label = "template path"
|
||||
|
||||
@signalcommand
|
||||
def handle_label(self, template_path, **options):
|
||||
try:
|
||||
template = loader.get_template(template_path).template
|
||||
except TemplateDoesNotExist:
|
||||
sys.stderr.write("No template found\n")
|
||||
else:
|
||||
sys.stdout.write(self.style.SUCCESS((template.name)))
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import argparse
|
||||
import string
|
||||
import secrets
|
||||
from typing import List
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Generates a simple new password that can be used for a user password. "
|
||||
"Uses Python’s secrets module to generate passwords. Do not use this command to "
|
||||
"generate your most secure passwords."
|
||||
|
||||
requires_system_checks: List[str] = []
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"-l", "--length", nargs="?", type=int, default=16, help="Password length."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--complex",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="More complex alphabet, includes punctuation",
|
||||
)
|
||||
|
||||
@signalcommand
|
||||
def handle(self, *args, **options):
|
||||
length = options["length"]
|
||||
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
if options["complex"]:
|
||||
alphabet += string.punctuation
|
||||
return "".join(secrets.choice(alphabet) for i in range(length))
|
||||
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import List
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.management.utils import get_random_secret_key
|
||||
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Generates a new SECRET_KEY that can be used in a project settings file."
|
||||
|
||||
requires_system_checks: List[str] = []
|
||||
|
||||
@signalcommand
|
||||
def handle(self, *args, **options):
|
||||
return get_random_secret_key()
|
||||
@@ -0,0 +1,486 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.template import loader
|
||||
|
||||
from django_extensions.management.modelviz import ModelGraph, generate_dot
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
try:
|
||||
import pygraphviz
|
||||
|
||||
HAS_PYGRAPHVIZ = True
|
||||
except ImportError:
|
||||
HAS_PYGRAPHVIZ = False
|
||||
|
||||
try:
|
||||
try:
|
||||
import pydotplus as pydot
|
||||
except ImportError:
|
||||
import pydot
|
||||
HAS_PYDOT = True
|
||||
except ImportError:
|
||||
HAS_PYDOT = False
|
||||
|
||||
|
||||
def retheme(graph_data, app_style={}):
|
||||
if isinstance(app_style, str):
|
||||
if os.path.exists(app_style):
|
||||
try:
|
||||
with open(app_style, "rt") as f:
|
||||
app_style = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Invalid app style file {app_style}")
|
||||
raise Exception(e)
|
||||
else:
|
||||
return graph_data
|
||||
|
||||
for gc in graph_data["graphs"]:
|
||||
for g in gc:
|
||||
if "name" in g:
|
||||
for m in g["models"]:
|
||||
app_name = g["app_name"]
|
||||
if app_name in app_style:
|
||||
m["style"] = app_style[app_name]
|
||||
return graph_data
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates a GraphViz dot file for the specified app names."
|
||||
" You can pass multiple app names and they will all be combined into a"
|
||||
" single model. Output is usually directed to a dot file."
|
||||
|
||||
can_import_settings = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Allow defaults for arguments to be set in settings.GRAPH_MODELS.
|
||||
|
||||
Each argument in self.arguments is a dict where the key is the
|
||||
space-separated args and the value is our kwarg dict.
|
||||
|
||||
The default from settings is keyed as the long arg name with '--'
|
||||
removed and any '-' replaced by '_'. For example, the default value for
|
||||
--disable-fields can be set in settings.GRAPH_MODELS['disable_fields'].
|
||||
"""
|
||||
self.arguments = {
|
||||
"--app-style": {
|
||||
"action": "store",
|
||||
"help": "Path to style json to configure the style per app",
|
||||
"dest": "app-style",
|
||||
"default": ".app-style.json",
|
||||
},
|
||||
"--pygraphviz": {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"dest": "pygraphviz",
|
||||
"help": "Output graph data as image using PyGraphViz.",
|
||||
},
|
||||
"--pydot": {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"dest": "pydot",
|
||||
"help": "Output graph data as image using PyDot(Plus).",
|
||||
},
|
||||
"--dot": {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"dest": "dot",
|
||||
"help": (
|
||||
"Output graph data as raw DOT (graph description language) "
|
||||
"text data."
|
||||
),
|
||||
},
|
||||
"--json": {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"dest": "json",
|
||||
"help": "Output graph data as JSON",
|
||||
},
|
||||
"--disable-fields -d": {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"dest": "disable_fields",
|
||||
"help": "Do not show the class member fields",
|
||||
},
|
||||
"--disable-abstract-fields": {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"dest": "disable_abstract_fields",
|
||||
"help": "Do not show the class member fields that were inherited",
|
||||
},
|
||||
"--display-field-choices": {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"dest": "display_field_choices",
|
||||
"help": "Display choices instead of field type",
|
||||
},
|
||||
"--group-models -g": {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"dest": "group_models",
|
||||
"help": "Group models together respective to their application",
|
||||
},
|
||||
"--all-applications -a": {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"dest": "all_applications",
|
||||
"help": "Automatically include all applications from INSTALLED_APPS",
|
||||
},
|
||||
"--output -o": {
|
||||
"action": "store",
|
||||
"dest": "outputfile",
|
||||
"help": (
|
||||
"Render output file. Type of output dependend on file extensions. "
|
||||
"Use png or jpg to render graph to image."
|
||||
),
|
||||
},
|
||||
"--layout -l": {
|
||||
"action": "store",
|
||||
"dest": "layout",
|
||||
"default": "dot",
|
||||
"help": "Layout to be used by GraphViz for visualization. Layouts: "
|
||||
"circo dot fdp neato nop nop1 nop2 twopi",
|
||||
},
|
||||
"--theme -t": {
|
||||
"action": "store",
|
||||
"dest": "theme",
|
||||
"default": "django2018",
|
||||
"help": "Theme to use. Supplied are 'original' and 'django2018'. "
|
||||
"You can create your own by creating dot templates in "
|
||||
"'django_extentions/graph_models/themename/' template directory.",
|
||||
},
|
||||
"--verbose-names -n": {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"dest": "verbose_names",
|
||||
"help": "Use verbose_name of models and fields",
|
||||
},
|
||||
"--language -L": {
|
||||
"action": "store",
|
||||
"dest": "language",
|
||||
"help": "Specify language used for verbose_name localization",
|
||||
},
|
||||
"--exclude-columns -x": {
|
||||
"action": "store",
|
||||
"dest": "exclude_columns",
|
||||
"help": "Exclude specific column(s) from the graph. "
|
||||
"Can also load exclude list from file.",
|
||||
},
|
||||
"--exclude-models -X": {
|
||||
"action": "store",
|
||||
"dest": "exclude_models",
|
||||
"help": "Exclude specific model(s) from the graph. Can also load "
|
||||
"exclude list from file. Wildcards (*) are allowed.",
|
||||
},
|
||||
"--include-models -I": {
|
||||
"action": "store",
|
||||
"dest": "include_models",
|
||||
"help": "Restrict the graph to specified models. "
|
||||
"Wildcards (*) are allowed.",
|
||||
},
|
||||
"--inheritance -e": {
|
||||
"action": "store_true",
|
||||
"default": True,
|
||||
"dest": "inheritance",
|
||||
"help": "Include inheritance arrows (default)",
|
||||
},
|
||||
"--no-inheritance -E": {
|
||||
"action": "store_false",
|
||||
"default": False,
|
||||
"dest": "inheritance",
|
||||
"help": "Do not include inheritance arrows",
|
||||
},
|
||||
"--hide-relations-from-fields -R": {
|
||||
"action": "store_false",
|
||||
"default": True,
|
||||
"dest": "relations_as_fields",
|
||||
"help": "Do not show relations as fields in the graph.",
|
||||
},
|
||||
"--relation-fields-only": {
|
||||
"action": "store",
|
||||
"default": False,
|
||||
"dest": "relation_fields_only",
|
||||
"help": "Only display fields that are relevant for relations",
|
||||
},
|
||||
"--disable-sort-fields -S": {
|
||||
"action": "store_false",
|
||||
"default": True,
|
||||
"dest": "sort_fields",
|
||||
"help": "Do not sort fields",
|
||||
},
|
||||
"--hide-edge-labels": {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"dest": "hide_edge_labels",
|
||||
"help": "Do not show relations labels in the graph.",
|
||||
},
|
||||
"--arrow-shape": {
|
||||
"action": "store",
|
||||
"default": "dot",
|
||||
"dest": "arrow_shape",
|
||||
"choices": [
|
||||
"box",
|
||||
"crow",
|
||||
"curve",
|
||||
"icurve",
|
||||
"diamond",
|
||||
"dot",
|
||||
"inv",
|
||||
"none",
|
||||
"normal",
|
||||
"tee",
|
||||
"vee",
|
||||
],
|
||||
"help": "Arrow shape to use for relations. Default is dot. "
|
||||
"Available shapes: box, crow, curve, icurve, diamond, dot, inv, "
|
||||
"none, normal, tee, vee.",
|
||||
},
|
||||
"--color-code-deletions": {
|
||||
"action": "store_true",
|
||||
"default": False,
|
||||
"dest": "color_code_deletions",
|
||||
"help": "Color the relations according to their on_delete setting, "
|
||||
"where it is applicable. The colors are: red (CASCADE), "
|
||||
"orange (SET_NULL), green (SET_DEFAULT), yellow (SET), "
|
||||
"blue (PROTECT), grey (DO_NOTHING), and purple (RESTRICT).",
|
||||
},
|
||||
"--rankdir": {
|
||||
"action": "store",
|
||||
"default": "TB",
|
||||
"dest": "rankdir",
|
||||
"choices": ["TB", "BT", "LR", "RL"],
|
||||
"help": "Set direction of graph layout. Supported directions: "
|
||||
"TB, LR, BT and RL. Corresponding to directed graphs drawn from "
|
||||
"top to bottom, from left to right, from bottom to top, and from "
|
||||
"right to left, respectively. Default is TB.",
|
||||
},
|
||||
"--ordering": {
|
||||
"action": "store",
|
||||
"default": None,
|
||||
"dest": "ordering",
|
||||
"choices": ["in", "out"],
|
||||
"help": "Controls how the edges are arranged. Supported orderings: "
|
||||
'"in" (incoming relations first), "out" (outgoing relations first). '
|
||||
"Default is None.",
|
||||
},
|
||||
}
|
||||
|
||||
defaults = getattr(settings, "GRAPH_MODELS", None)
|
||||
|
||||
if defaults:
|
||||
for argument in self.arguments:
|
||||
arg_split = argument.split(" ")
|
||||
setting_opt = arg_split[0].lstrip("-").replace("-", "_")
|
||||
if setting_opt in defaults:
|
||||
self.arguments[argument]["default"] = defaults[setting_opt]
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Unpack self.arguments for parser.add_arguments."""
|
||||
parser.add_argument("app_label", nargs="*")
|
||||
for argument in self.arguments:
|
||||
parser.add_argument(*argument.split(" "), **self.arguments[argument])
|
||||
|
||||
@signalcommand
|
||||
def handle(self, *args, **options):
|
||||
args = options["app_label"]
|
||||
if not args and not options["all_applications"]:
|
||||
default_app_labels = getattr(settings, "GRAPH_MODELS", {}).get("app_labels")
|
||||
if default_app_labels:
|
||||
args = default_app_labels
|
||||
else:
|
||||
raise CommandError("need one or more arguments for appname")
|
||||
|
||||
# Determine output format based on options, file extension, and library
|
||||
# availability.
|
||||
outputfile = options.get("outputfile") or ""
|
||||
_, outputfile_ext = os.path.splitext(outputfile)
|
||||
outputfile_ext = outputfile_ext.lower()
|
||||
output_opts_names = ["pydot", "pygraphviz", "json", "dot"]
|
||||
output_opts = {k: v for k, v in options.items() if k in output_opts_names}
|
||||
output_opts_count = sum(output_opts.values())
|
||||
if output_opts_count > 1:
|
||||
raise CommandError(
|
||||
"Only one of %s can be set."
|
||||
% ", ".join(["--%s" % opt for opt in output_opts_names])
|
||||
)
|
||||
|
||||
if output_opts_count == 1:
|
||||
output = next(key for key, val in output_opts.items() if val)
|
||||
elif not outputfile:
|
||||
# When neither outputfile nor a output format option are set,
|
||||
# default to printing .dot format to stdout. Kept for backward
|
||||
# compatibility.
|
||||
output = "dot"
|
||||
elif outputfile_ext == ".dot":
|
||||
output = "dot"
|
||||
elif outputfile_ext == ".json":
|
||||
output = "json"
|
||||
elif HAS_PYGRAPHVIZ:
|
||||
output = "pygraphviz"
|
||||
elif HAS_PYDOT:
|
||||
output = "pydot"
|
||||
else:
|
||||
raise CommandError(
|
||||
"Neither pygraphviz nor pydotplus could be found to generate the image."
|
||||
" To generate text output, use the --json or --dot options."
|
||||
)
|
||||
|
||||
if options.get("rankdir") != "TB" and output not in [
|
||||
"pydot",
|
||||
"pygraphviz",
|
||||
"dot",
|
||||
]:
|
||||
raise CommandError(
|
||||
"--rankdir is not supported for the chosen output format"
|
||||
)
|
||||
|
||||
if options.get("ordering") and output not in ["pydot", "pygraphviz", "dot"]:
|
||||
raise CommandError(
|
||||
"--ordering is not supported for the chosen output format"
|
||||
)
|
||||
|
||||
# Consistency check: Abort if --pygraphviz or --pydot options are set
|
||||
# but no outputfile is specified. Before 2.1.4 this silently fell back
|
||||
# to printind .dot format to stdout.
|
||||
if output in ["pydot", "pygraphviz"] and not outputfile:
|
||||
raise CommandError(
|
||||
"An output file (--output) must be specified when --pydot or "
|
||||
"--pygraphviz are set."
|
||||
)
|
||||
|
||||
cli_options = " ".join(sys.argv[2:])
|
||||
graph_models = ModelGraph(args, cli_options=cli_options, **options)
|
||||
graph_models.generate_graph_data()
|
||||
|
||||
if output == "json":
|
||||
graph_data = graph_models.get_graph_data(as_json=True)
|
||||
return self.render_output_json(graph_data, outputfile)
|
||||
|
||||
graph_data = graph_models.get_graph_data(as_json=False)
|
||||
|
||||
theme = options["theme"]
|
||||
template_name = os.path.join(
|
||||
"django_extensions", "graph_models", theme, "digraph.dot"
|
||||
)
|
||||
template = loader.get_template(template_name)
|
||||
|
||||
graph_data = retheme(graph_data, app_style=options["app-style"])
|
||||
dotdata = generate_dot(graph_data, template=template)
|
||||
|
||||
if output == "pygraphviz":
|
||||
return self.render_output_pygraphviz(dotdata, **options)
|
||||
if output == "pydot":
|
||||
return self.render_output_pydot(dotdata, **options)
|
||||
self.print_output(dotdata, outputfile)
|
||||
|
||||
def print_output(self, dotdata, output_file=None):
|
||||
"""Write model data to file or stdout in DOT (text) format."""
|
||||
if isinstance(dotdata, bytes):
|
||||
dotdata = dotdata.decode()
|
||||
|
||||
if output_file:
|
||||
with open(output_file, "wt") as dot_output_f:
|
||||
dot_output_f.write(dotdata)
|
||||
else:
|
||||
self.stdout.write(dotdata)
|
||||
|
||||
def render_output_json(self, graph_data, output_file=None):
|
||||
"""Write model data to file or stdout in JSON format."""
|
||||
if output_file:
|
||||
with open(output_file, "wt") as json_output_f:
|
||||
json.dump(graph_data, json_output_f)
|
||||
else:
|
||||
self.stdout.write(json.dumps(graph_data))
|
||||
|
||||
def render_output_pygraphviz(self, dotdata, **kwargs):
|
||||
"""Render model data as image using pygraphviz."""
|
||||
if not HAS_PYGRAPHVIZ:
|
||||
raise CommandError("You need to install pygraphviz python module")
|
||||
|
||||
version = pygraphviz.__version__.rstrip("-svn")
|
||||
try:
|
||||
if tuple(int(v) for v in version.split(".")) < (0, 36):
|
||||
# HACK around old/broken AGraph before version 0.36
|
||||
# (ubuntu ships with this old version)
|
||||
tmpfile = tempfile.NamedTemporaryFile()
|
||||
tmpfile.write(dotdata)
|
||||
tmpfile.seek(0)
|
||||
dotdata = tmpfile.name
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
graph = pygraphviz.AGraph(dotdata)
|
||||
graph.layout(prog=kwargs["layout"])
|
||||
graph.draw(kwargs["outputfile"])
|
||||
|
||||
def render_output_pydot(self, dotdata, **kwargs):
|
||||
"""Render model data as image using pydot."""
|
||||
if not HAS_PYDOT:
|
||||
raise CommandError("You need to install pydot python module")
|
||||
|
||||
graph = pydot.graph_from_dot_data(dotdata)
|
||||
if not graph:
|
||||
raise CommandError("pydot returned an error")
|
||||
if isinstance(graph, (list, tuple)):
|
||||
if len(graph) > 1:
|
||||
sys.stderr.write(
|
||||
"Found more then one graph, rendering only the first one.\n"
|
||||
)
|
||||
graph = graph[0]
|
||||
|
||||
output_file = kwargs["outputfile"]
|
||||
formats = [
|
||||
"bmp",
|
||||
"canon",
|
||||
"cmap",
|
||||
"cmapx",
|
||||
"cmapx_np",
|
||||
"dot",
|
||||
"dia",
|
||||
"emf",
|
||||
"em",
|
||||
"fplus",
|
||||
"eps",
|
||||
"fig",
|
||||
"gd",
|
||||
"gd2",
|
||||
"gif",
|
||||
"gv",
|
||||
"imap",
|
||||
"imap_np",
|
||||
"ismap",
|
||||
"jpe",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"metafile",
|
||||
"pdf",
|
||||
"pic",
|
||||
"plain",
|
||||
"plain-ext",
|
||||
"png",
|
||||
"pov",
|
||||
"ps",
|
||||
"ps2",
|
||||
"svg",
|
||||
"svgz",
|
||||
"tif",
|
||||
"tiff",
|
||||
"tk",
|
||||
"vml",
|
||||
"vmlz",
|
||||
"vrml",
|
||||
"wbmp",
|
||||
"webp",
|
||||
"xdot",
|
||||
]
|
||||
ext = output_file[output_file.rfind(".") + 1 :]
|
||||
format_ = ext if ext in formats else "raw"
|
||||
graph.write(output_file, format=format_)
|
||||
@@ -0,0 +1,185 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Author: OmenApps. https://omenapps.com
|
||||
import inspect
|
||||
|
||||
from django.apps import apps as django_apps
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import connection
|
||||
from django_extensions.management.color import color_style
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
TAB = " "
|
||||
HALFTAB = " "
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""A simple management command which lists model fields and methods."""
|
||||
|
||||
help = "List out the fields and methods for each model"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
super().add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--field-class",
|
||||
action="store_true",
|
||||
default=None,
|
||||
help="show class name of field.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--db-type",
|
||||
action="store_true",
|
||||
default=None,
|
||||
help="show database column type of field.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--signature",
|
||||
action="store_true",
|
||||
default=None,
|
||||
help="show the signature of method.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all-methods",
|
||||
action="store_true",
|
||||
default=None,
|
||||
help="list all methods, including private and default.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
nargs="?",
|
||||
type=str,
|
||||
default=None,
|
||||
help="list the details for a single model. "
|
||||
"Input should be in the form appname.Modelname",
|
||||
)
|
||||
|
||||
def list_model_info(self, options):
|
||||
style = color_style()
|
||||
INFO = getattr(style, "INFO", lambda x: x)
|
||||
WARN = getattr(style, "WARN", lambda x: x)
|
||||
BOLD = getattr(style, "BOLD", lambda x: x)
|
||||
|
||||
FIELD_CLASS = (
|
||||
True
|
||||
if options.get("field_class", None) is not None
|
||||
else getattr(settings, "MODEL_INFO_FIELD_CLASS", False)
|
||||
)
|
||||
DB_TYPE = (
|
||||
True
|
||||
if options.get("db_type", None) is not None
|
||||
else getattr(settings, "MODEL_INFO_DB_TYPE", False)
|
||||
)
|
||||
SIGNATURE = (
|
||||
True
|
||||
if options.get("signature", None) is not None
|
||||
else getattr(settings, "MODEL_INFO_SIGNATURE", False)
|
||||
)
|
||||
ALL_METHODS = (
|
||||
True
|
||||
if options.get("all_methods", None) is not None
|
||||
else getattr(settings, "MODEL_INFO_ALL_METHODS", False)
|
||||
)
|
||||
MODEL = (
|
||||
options.get("model")
|
||||
if options.get("model", None) is not None
|
||||
else getattr(settings, "MODEL_INFO_MODEL", False)
|
||||
)
|
||||
|
||||
default_methods = [
|
||||
"check",
|
||||
"clean",
|
||||
"clean_fields",
|
||||
"date_error_message",
|
||||
"delete",
|
||||
"from_db",
|
||||
"full_clean",
|
||||
"get_absolute_url",
|
||||
"get_deferred_fields",
|
||||
"prepare_database_save",
|
||||
"refresh_from_db",
|
||||
"save",
|
||||
"save_base",
|
||||
"serializable_value",
|
||||
"unique_error_message",
|
||||
"validate_unique",
|
||||
]
|
||||
|
||||
if MODEL:
|
||||
model_list = [django_apps.get_model(MODEL)]
|
||||
else:
|
||||
model_list = sorted(
|
||||
django_apps.get_models(),
|
||||
key=lambda x: (x._meta.app_label, x._meta.object_name),
|
||||
reverse=False,
|
||||
)
|
||||
for model in model_list:
|
||||
self.stdout.write(
|
||||
INFO(model._meta.app_label + "." + model._meta.object_name)
|
||||
)
|
||||
self.stdout.write(BOLD(HALFTAB + "Fields:"))
|
||||
|
||||
for field in model._meta.get_fields():
|
||||
field_info = TAB + field.name + " -"
|
||||
|
||||
if FIELD_CLASS:
|
||||
try:
|
||||
field_info += " " + field.__class__.__name__
|
||||
except TypeError:
|
||||
field_info += WARN(" TypeError (field_class)")
|
||||
except AttributeError:
|
||||
field_info += WARN(" AttributeError (field_class)")
|
||||
if FIELD_CLASS and DB_TYPE:
|
||||
field_info += ","
|
||||
if DB_TYPE:
|
||||
try:
|
||||
field_info += " " + field.db_type(connection=connection)
|
||||
except TypeError:
|
||||
field_info += WARN(" TypeError (db_type)")
|
||||
except AttributeError:
|
||||
field_info += WARN(" AttributeError (db_type)")
|
||||
|
||||
self.stdout.write(field_info)
|
||||
|
||||
if ALL_METHODS:
|
||||
self.stdout.write(BOLD(HALFTAB + "Methods (all):"))
|
||||
else:
|
||||
self.stdout.write(BOLD(HALFTAB + "Methods (non-private/internal):"))
|
||||
|
||||
for method_name in dir(model):
|
||||
try:
|
||||
method = getattr(model, method_name)
|
||||
if ALL_METHODS:
|
||||
if callable(method) and not method_name[0].isupper():
|
||||
if SIGNATURE:
|
||||
signature = inspect.signature(method)
|
||||
else:
|
||||
signature = "()"
|
||||
self.stdout.write(TAB + method_name + str(signature))
|
||||
else:
|
||||
if (
|
||||
callable(method)
|
||||
and not method_name.startswith("_")
|
||||
and method_name not in default_methods
|
||||
and not method_name[0].isupper()
|
||||
):
|
||||
if SIGNATURE:
|
||||
signature = inspect.signature(method)
|
||||
else:
|
||||
signature = "()"
|
||||
self.stdout.write(TAB + method_name + str(signature))
|
||||
except AttributeError:
|
||||
self.stdout.write(TAB + method_name + WARN(" - AttributeError"))
|
||||
except ValueError:
|
||||
self.stdout.write(
|
||||
TAB
|
||||
+ method_name
|
||||
+ WARN(" - ValueError (could not identify signature)")
|
||||
)
|
||||
|
||||
self.stdout.write("\n")
|
||||
|
||||
self.stdout.write(INFO("Total Models Listed: %d" % len(model_list)))
|
||||
|
||||
@signalcommand
|
||||
def handle(self, *args, **options):
|
||||
self.list_model_info(options)
|
||||
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Based on https://gist.github.com/voldmar/1264102
|
||||
# and https://gist.github.com/runekaagaard/2eecf0a8367959dc634b7866694daf2c
|
||||
|
||||
import gc
|
||||
import inspect
|
||||
import weakref
|
||||
from collections import defaultdict
|
||||
|
||||
import django
|
||||
from django.apps import apps
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models.signals import (
|
||||
ModelSignal,
|
||||
pre_init,
|
||||
post_init,
|
||||
pre_save,
|
||||
post_save,
|
||||
pre_delete,
|
||||
post_delete,
|
||||
m2m_changed,
|
||||
pre_migrate,
|
||||
post_migrate,
|
||||
)
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
|
||||
MSG = "{module}.{name} #{line}{is_async}"
|
||||
|
||||
SIGNAL_NAMES = {
|
||||
pre_init: "pre_init",
|
||||
post_init: "post_init",
|
||||
pre_save: "pre_save",
|
||||
post_save: "post_save",
|
||||
pre_delete: "pre_delete",
|
||||
post_delete: "post_delete",
|
||||
m2m_changed: "m2m_changed",
|
||||
pre_migrate: "pre_migrate",
|
||||
post_migrate: "post_migrate",
|
||||
}
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "List all signals by model and signal type"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
all_models = apps.get_models(include_auto_created=True, include_swapped=True)
|
||||
model_lookup = {id(m): m for m in all_models}
|
||||
|
||||
signals = [obj for obj in gc.get_objects() if isinstance(obj, ModelSignal)]
|
||||
models = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
for signal in signals:
|
||||
signal_name = SIGNAL_NAMES.get(signal, "unknown")
|
||||
for receiver in signal.receivers:
|
||||
if django.VERSION >= (5, 0):
|
||||
lookup, receiver, is_async = receiver
|
||||
else:
|
||||
lookup, receiver = receiver
|
||||
is_async = False
|
||||
if isinstance(receiver, weakref.ReferenceType):
|
||||
receiver = receiver()
|
||||
if receiver is None:
|
||||
continue
|
||||
receiver_id, sender_id = lookup
|
||||
|
||||
model = model_lookup.get(sender_id, "_unknown_")
|
||||
if model:
|
||||
models[model][signal_name].append(
|
||||
MSG.format(
|
||||
name=receiver.__name__,
|
||||
module=receiver.__module__,
|
||||
is_async=" (async)" if is_async else "",
|
||||
line=inspect.getsourcelines(receiver)[1],
|
||||
path=inspect.getsourcefile(receiver),
|
||||
)
|
||||
)
|
||||
|
||||
output = []
|
||||
for key in sorted(models.keys(), key=str):
|
||||
verbose_name = force_str(key._meta.verbose_name)
|
||||
output.append(
|
||||
"{}.{} ({})".format(key.__module__, key.__name__, verbose_name)
|
||||
)
|
||||
for signal_name in sorted(models[key].keys()):
|
||||
lines = models[key][signal_name]
|
||||
output.append(" {}".format(signal_name))
|
||||
for line in lines:
|
||||
output.append(" {}".format(line))
|
||||
|
||||
return "\n".join(output)
|
||||
@@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
try:
|
||||
from aiosmtpd.controller import Controller
|
||||
except ImportError:
|
||||
raise ImportError("Please install 'aiosmtpd' library to use mail_debug command.")
|
||||
|
||||
from logging import getLogger
|
||||
from typing import List
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from django_extensions.management.utils import setup_logger, signalcommand
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class CustomHandler:
|
||||
async def handle_DATA(self, server, session, envelope):
|
||||
"""Output will be sent to the module logger at INFO level."""
|
||||
peer = session.peer
|
||||
inheaders = 1
|
||||
lines = envelope.content.decode("utf8", errors="replace").splitlines()
|
||||
logger.info("---------- MESSAGE FOLLOWS ----------")
|
||||
for line in lines:
|
||||
# headers first
|
||||
if inheaders and not line:
|
||||
logger.info("X-Peer: %s" % peer[0])
|
||||
inheaders = 0
|
||||
logger.info(line)
|
||||
logger.info("------------ END MESSAGE ------------")
|
||||
return "250 OK"
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Starts a test mail server for development."
|
||||
args = "[optional port number or ippaddr:port]"
|
||||
|
||||
requires_system_checks: List[str] = []
|
||||
|
||||
def add_arguments(self, parser):
|
||||
super().add_arguments(parser)
|
||||
parser.add_argument("addrport", nargs="?")
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
dest="output_file",
|
||||
default=None,
|
||||
help="Specifies an output file to send a copy of all messages "
|
||||
"(not flushed immediately).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-settings",
|
||||
dest="use_settings",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Uses EMAIL_HOST and HOST_PORT from Django settings.",
|
||||
)
|
||||
|
||||
@signalcommand
|
||||
def handle(self, addrport="", *args, **options):
|
||||
if not addrport:
|
||||
if options["use_settings"]:
|
||||
from django.conf import settings
|
||||
|
||||
addr = getattr(settings, "EMAIL_HOST", "")
|
||||
port = str(getattr(settings, "EMAIL_PORT", "1025"))
|
||||
else:
|
||||
addr = ""
|
||||
port = "1025"
|
||||
else:
|
||||
try:
|
||||
addr, port = addrport.split(":")
|
||||
except ValueError:
|
||||
addr, port = "", addrport
|
||||
if not addr:
|
||||
addr = "127.0.0.1"
|
||||
|
||||
if not port.isdigit():
|
||||
raise CommandError("%r is not a valid port number." % port)
|
||||
else:
|
||||
port = int(port)
|
||||
|
||||
# Add console handler
|
||||
setup_logger(logger, stream=self.stdout, filename=options["output_file"])
|
||||
|
||||
def inner_run():
|
||||
quit_command = (sys.platform == "win32") and "CTRL-BREAK" or "CONTROL-C"
|
||||
print(
|
||||
"Now accepting mail at %s:%s -- use %s to quit"
|
||||
% (addr, port, quit_command)
|
||||
)
|
||||
handler = CustomHandler()
|
||||
controller = Controller(handler, hostname=addr, port=port)
|
||||
controller.start()
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_forever()
|
||||
|
||||
try:
|
||||
inner_run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
@@ -0,0 +1,204 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
from operator import itemgetter
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import DEFAULT_DB_ALIAS, connections
|
||||
from django.db.backends.base.base import BaseDatabaseWrapper
|
||||
from django.db.migrations.loader import MigrationLoader
|
||||
from django.db.migrations.recorder import MigrationRecorder
|
||||
from django.utils import timezone
|
||||
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
DEFAULT_FILENAME = "managestate.json"
|
||||
DEFAULT_STATE = "default"
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Manage database state in the convenient way."
|
||||
_applied_migrations = None
|
||||
migrate_args: dict
|
||||
migrate_options: dict
|
||||
filename: str
|
||||
verbosity: int
|
||||
database: str
|
||||
conn: BaseDatabaseWrapper
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"action",
|
||||
choices=("dump", "load"),
|
||||
help="An action to do. "
|
||||
"Dump action saves applied migrations to a file. "
|
||||
"Load action applies migrations specified in a file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"state",
|
||||
nargs="?",
|
||||
default=DEFAULT_STATE,
|
||||
help="A name of a state. Usually a name of a git branch."
|
||||
f'Defaults to "{DEFAULT_STATE}"',
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--database",
|
||||
default=DEFAULT_DB_ALIAS,
|
||||
help="Nominates a database to synchronize. "
|
||||
f'Defaults to the "{DEFAULT_DB_ALIAS}" database.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--filename",
|
||||
default=DEFAULT_FILENAME,
|
||||
help=f'A file to write to. Defaults to "{DEFAULT_FILENAME}"',
|
||||
)
|
||||
|
||||
# migrate command arguments
|
||||
parser.add_argument(
|
||||
"--noinput",
|
||||
"--no-input",
|
||||
action="store_false",
|
||||
dest="interactive",
|
||||
help='The argument for "migrate" command. '
|
||||
"Tells Django to NOT prompt the user for input of any kind.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fake",
|
||||
action="store_true",
|
||||
help='The argument for "migrate" command. '
|
||||
"Mark migrations as run without actually running them.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fake-initial",
|
||||
action="store_true",
|
||||
help='The argument for "migrate" command. '
|
||||
"Detect if tables already exist and fake-apply initial migrations if so. "
|
||||
"Make sure that the current database schema matches your initial migration "
|
||||
"before using this flag. "
|
||||
"Django will only check for an existing table name.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--plan",
|
||||
action="store_true",
|
||||
help='The argument for "migrate" command. '
|
||||
"Shows a list of the migration actions that will be performed.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--run-syncdb",
|
||||
action="store_true",
|
||||
help='The argument for "migrate" command. '
|
||||
"Creates tables for apps without migrations.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
dest="check_unapplied",
|
||||
help='The argument for "migrate" command. '
|
||||
"Exits with a non-zero status if unapplied migrations exist.",
|
||||
)
|
||||
|
||||
@signalcommand
|
||||
def handle(self, action, database, filename, state, *args, **options):
|
||||
self.migrate_args = args
|
||||
self.migrate_options = options
|
||||
self.verbosity = options["verbosity"]
|
||||
self.conn = connections[database]
|
||||
self.database = database
|
||||
self.filename = filename
|
||||
getattr(self, action)(state)
|
||||
|
||||
def dump(self, state: str):
|
||||
"""Save applied migrations to a file."""
|
||||
migrated_apps = self.get_migrated_apps()
|
||||
migrated_apps.update(self.get_applied_migrations())
|
||||
self.write({state: migrated_apps})
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'Migrations for state "{state}" have been successfully '
|
||||
f"saved to {self.filename}."
|
||||
)
|
||||
)
|
||||
|
||||
def load(self, state: str):
|
||||
"""Apply migrations from a file."""
|
||||
migrations = self.read().get(state)
|
||||
if migrations is None:
|
||||
raise CommandError(f"No such state saved: {state}")
|
||||
|
||||
kwargs = {
|
||||
**self.migrate_options,
|
||||
"database": self.database,
|
||||
"verbosity": self.verbosity - 1 if self.verbosity > 1 else 0,
|
||||
}
|
||||
|
||||
for app, migration in migrations.items():
|
||||
if self.is_applied(app, migration):
|
||||
continue
|
||||
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f'Applying migrations for "{app}"')
|
||||
)
|
||||
args = (app, migration, *self.migrate_args)
|
||||
call_command("migrate", *args, **kwargs)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'Migrations for "{state}" have been successfully applied.'
|
||||
)
|
||||
)
|
||||
|
||||
def get_migrated_apps(self) -> dict:
|
||||
"""Installed apps having migrations."""
|
||||
apps = MigrationLoader(self.conn).migrated_apps
|
||||
migrated_apps = dict.fromkeys(apps, "zero")
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write(
|
||||
"Apps having migrations: " + ", ".join(sorted(migrated_apps))
|
||||
)
|
||||
return migrated_apps
|
||||
|
||||
def get_applied_migrations(self) -> dict:
|
||||
"""Installed apps with last applied migrations."""
|
||||
if self._applied_migrations:
|
||||
return self._applied_migrations
|
||||
|
||||
migrations = MigrationRecorder(self.conn).applied_migrations()
|
||||
last_applied = sorted(migrations.keys(), key=itemgetter(1))
|
||||
|
||||
self._applied_migrations = dict(last_applied)
|
||||
return self._applied_migrations
|
||||
|
||||
def is_applied(self, app: str, migration: str) -> bool:
|
||||
"""Check whether a migration for an app is applied or not."""
|
||||
applied = self.get_applied_migrations().get(app)
|
||||
if applied == migration:
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f'Migrations for "{app}" are already applied.')
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
def read(self) -> dict:
|
||||
"""Get saved state from the file."""
|
||||
path = Path(self.filename)
|
||||
if not path.exists() or not path.is_file():
|
||||
raise CommandError(f"No such file: {self.filename}")
|
||||
|
||||
with open(self.filename) as file:
|
||||
return json.load(file)
|
||||
|
||||
def write(self, data: dict):
|
||||
"""Write new data to the file using existent one."""
|
||||
try:
|
||||
saved = self.read()
|
||||
except CommandError:
|
||||
saved = {}
|
||||
|
||||
saved.update(data, updated_at=str(timezone.now()))
|
||||
with open(self.filename, "w") as file:
|
||||
json.dump(saved, file, indent=2, sort_keys=True)
|
||||
@@ -0,0 +1,235 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.core.management import BaseCommand
|
||||
from django.db import transaction
|
||||
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
|
||||
def get_model_to_deduplicate():
|
||||
models = apps.get_models()
|
||||
iterator = 1
|
||||
for model in models:
|
||||
print("%s. %s" % (iterator, model.__name__))
|
||||
iterator += 1
|
||||
model_choice = int(
|
||||
input("Enter the number of the model you would like to de-duplicate:")
|
||||
)
|
||||
model_to_deduplicate = models[model_choice - 1]
|
||||
return model_to_deduplicate
|
||||
|
||||
|
||||
def get_field_names(model):
|
||||
fields = [field.name for field in model._meta.get_fields()]
|
||||
iterator = 1
|
||||
for field in fields:
|
||||
print("%s. %s" % (iterator, field))
|
||||
iterator += 1
|
||||
validated = False
|
||||
while not validated:
|
||||
first_field = int(
|
||||
input(
|
||||
"Enter the number of the (first) field you would like to de-duplicate."
|
||||
)
|
||||
)
|
||||
if first_field in range(1, iterator):
|
||||
validated = True
|
||||
else:
|
||||
print("Invalid input. Please try again.")
|
||||
fields_to_deduplicate = [fields[first_field - 1]]
|
||||
|
||||
done = False
|
||||
while not done:
|
||||
available_fields = [f for f in fields if f not in fields_to_deduplicate]
|
||||
iterator = 1
|
||||
for field in available_fields:
|
||||
print("%s. %s" % (iterator, field))
|
||||
iterator += 1
|
||||
print("C. Done adding fields.")
|
||||
|
||||
validated = False
|
||||
while not validated:
|
||||
print("You are currently deduplicating on the following fields:")
|
||||
print("\n".join(fields_to_deduplicate) + "\n")
|
||||
|
||||
additional_field = input("""
|
||||
Enter the number of the field you would like to de-duplicate.
|
||||
If you have entered all fields, enter C to continue.
|
||||
""")
|
||||
if additional_field == "C":
|
||||
done = True
|
||||
validated = True
|
||||
elif int(additional_field) in list(range(1, len(available_fields) + 1)):
|
||||
fields_to_deduplicate += [available_fields[int(additional_field) - 1]]
|
||||
validated = True
|
||||
else:
|
||||
print("Invalid input. Please try again.")
|
||||
|
||||
return fields_to_deduplicate
|
||||
|
||||
|
||||
def keep_first_or_last_instance():
|
||||
while True:
|
||||
first_or_last = input("""
|
||||
Do you want to keep the first or last duplicate instance?
|
||||
Enter "first" or "last" to continue.
|
||||
""")
|
||||
if first_or_last in ["first", "last"]:
|
||||
return first_or_last
|
||||
|
||||
|
||||
def get_generic_fields():
|
||||
"""Return a list of all GenericForeignKeys in all models."""
|
||||
generic_fields = []
|
||||
for model in apps.get_models():
|
||||
for field_name, field in model.__dict__.items():
|
||||
if isinstance(field, GenericForeignKey):
|
||||
generic_fields.append(field)
|
||||
return generic_fields
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """
|
||||
Removes duplicate model instances based on a specified
|
||||
model and field name(s).
|
||||
|
||||
Makes sure that any OneToOne, ForeignKey, or ManyToMany relationships
|
||||
attached to a deleted model(s) get reattached to the remaining model.
|
||||
|
||||
Based on the following:
|
||||
https://djangosnippets.org/snippets/2283/
|
||||
https://stackoverflow.com/a/41291137/2532070
|
||||
https://gist.github.com/edelvalle/01886b6f79ba0c4dce66
|
||||
"""
|
||||
|
||||
@signalcommand
|
||||
def handle(self, *args, **options):
|
||||
model = get_model_to_deduplicate()
|
||||
field_names = get_field_names(model)
|
||||
first_or_last = keep_first_or_last_instance()
|
||||
total_deleted_objects_count = 0
|
||||
for instance in model.objects.all():
|
||||
kwargs = {}
|
||||
for field_name in field_names:
|
||||
instance_field_value = instance.__getattribute__(field_name)
|
||||
kwargs.update({field_name: instance_field_value})
|
||||
try:
|
||||
model.objects.get(**kwargs)
|
||||
except model.MultipleObjectsReturned:
|
||||
instances = model.objects.filter(**kwargs)
|
||||
if first_or_last == "first":
|
||||
primary_object = instances.first()
|
||||
alias_objects = instances.exclude(pk=primary_object.pk)
|
||||
elif first_or_last == "last":
|
||||
primary_object = instances.last()
|
||||
alias_objects = instances.exclude(pk=primary_object.pk)
|
||||
|
||||
primary_object, deleted_objects, deleted_objects_count = (
|
||||
self.merge_model_instances(primary_object, alias_objects)
|
||||
)
|
||||
total_deleted_objects_count += deleted_objects_count
|
||||
|
||||
print(
|
||||
"Successfully deleted {} model instances.".format(
|
||||
total_deleted_objects_count
|
||||
)
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
def merge_model_instances(self, primary_object, alias_objects):
|
||||
"""
|
||||
Merge several model instances into one, the `primary_object`.
|
||||
Use this function to merge model objects and migrate all of the related
|
||||
fields from the alias objects the primary object.
|
||||
"""
|
||||
generic_fields = get_generic_fields()
|
||||
|
||||
# get related fields
|
||||
related_fields = list(
|
||||
filter(lambda x: x.is_relation is True, primary_object._meta.get_fields())
|
||||
)
|
||||
|
||||
many_to_many_fields = list(
|
||||
filter(lambda x: x.many_to_many is True, related_fields)
|
||||
)
|
||||
|
||||
related_fields = list(filter(lambda x: x.many_to_many is False, related_fields))
|
||||
|
||||
# Loop through all alias objects and migrate their references to the
|
||||
# primary object
|
||||
deleted_objects = []
|
||||
deleted_objects_count = 0
|
||||
for alias_object in alias_objects:
|
||||
# Migrate all foreign key references from alias object to primary
|
||||
# object.
|
||||
for many_to_many_field in many_to_many_fields:
|
||||
alias_varname = many_to_many_field.name
|
||||
related_objects = getattr(alias_object, alias_varname)
|
||||
for obj in related_objects.all():
|
||||
try:
|
||||
# Handle regular M2M relationships.
|
||||
getattr(alias_object, alias_varname).remove(obj)
|
||||
getattr(primary_object, alias_varname).add(obj)
|
||||
except AttributeError:
|
||||
# Handle M2M relationships with a 'through' model.
|
||||
# This does not delete the 'through model.
|
||||
# TODO: Allow the user to delete a duplicate 'through' model.
|
||||
through_model = getattr(alias_object, alias_varname).through
|
||||
kwargs = {
|
||||
many_to_many_field.m2m_reverse_field_name(): obj,
|
||||
many_to_many_field.m2m_field_name(): alias_object,
|
||||
}
|
||||
through_model_instances = through_model.objects.filter(**kwargs)
|
||||
for instance in through_model_instances:
|
||||
# Re-attach the through model to the primary_object
|
||||
setattr(
|
||||
instance,
|
||||
many_to_many_field.m2m_field_name(),
|
||||
primary_object,
|
||||
)
|
||||
instance.save()
|
||||
# TODO: Here, try to delete duplicate instances that are
|
||||
# disallowed by a unique_together constraint
|
||||
|
||||
for related_field in related_fields:
|
||||
if related_field.one_to_many:
|
||||
alias_varname = related_field.get_accessor_name()
|
||||
related_objects = getattr(alias_object, alias_varname)
|
||||
for obj in related_objects.all():
|
||||
field_name = related_field.field.name
|
||||
setattr(obj, field_name, primary_object)
|
||||
obj.save()
|
||||
elif related_field.one_to_one or related_field.many_to_one:
|
||||
alias_varname = related_field.name
|
||||
related_object = getattr(alias_object, alias_varname)
|
||||
primary_related_object = getattr(primary_object, alias_varname)
|
||||
if primary_related_object is None:
|
||||
setattr(primary_object, alias_varname, related_object)
|
||||
primary_object.save()
|
||||
elif related_field.one_to_one:
|
||||
self.stdout.write(
|
||||
"Deleted {} with id {}\n".format(
|
||||
related_object, related_object.id
|
||||
)
|
||||
)
|
||||
related_object.delete()
|
||||
|
||||
for field in generic_fields:
|
||||
filter_kwargs = {}
|
||||
filter_kwargs[field.fk_field] = alias_object._get_pk_val()
|
||||
filter_kwargs[field.ct_field] = field.get_content_type(alias_object)
|
||||
related_objects = field.model.objects.filter(**filter_kwargs)
|
||||
for generic_related_object in related_objects:
|
||||
setattr(generic_related_object, field.name, primary_object)
|
||||
generic_related_object.save()
|
||||
|
||||
if alias_object.id:
|
||||
deleted_objects += [alias_object]
|
||||
self.stdout.write(
|
||||
"Deleted {} with id {}\n".format(alias_object, alias_object.id)
|
||||
)
|
||||
alias_object.delete()
|
||||
deleted_objects_count += 1
|
||||
|
||||
return primary_object, deleted_objects, deleted_objects_count
|
||||
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from django_extensions.compat import get_template_setting
|
||||
from django_extensions.management.utils import signalcommand
|
||||
|
||||
ANNOTATION_RE = re.compile(
|
||||
r"\{?#[\s]*?(TODO|FIXME|BUG|HACK|WARNING|NOTE|XXX)[\s:]?(.+)"
|
||||
)
|
||||
ANNOTATION_END_RE = re.compile(r"(.*)#\}(.*)")
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Show all annotations like TODO, FIXME, BUG, HACK, WARNING, NOTE or XXX "
|
||||
"in your py and HTML files."
|
||||
label = "annotation tag (TODO, FIXME, BUG, HACK, WARNING, NOTE, XXX)"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
super().add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--tag", dest="tag", help="Search for specific tags only", action="append"
|
||||
)
|
||||
|
||||
@signalcommand
|
||||
def handle(self, *args, **options):
|
||||
# don't add django internal code
|
||||
apps = [
|
||||
app.replace(".", "/")
|
||||
for app in filter(
|
||||
lambda app: not app.startswith("django.contrib"),
|
||||
settings.INSTALLED_APPS,
|
||||
)
|
||||
]
|
||||
template_dirs = get_template_setting("DIRS", [])
|
||||
base_dir = getattr(settings, "BASE_DIR")
|
||||
if template_dirs:
|
||||
apps += template_dirs
|
||||
for app_dir in apps:
|
||||
if base_dir:
|
||||
app_dir = os.path.join(base_dir, app_dir)
|
||||
for top, dirs, files in os.walk(app_dir):
|
||||
for fn in files:
|
||||
if os.path.splitext(fn)[1] in (".py", ".html"):
|
||||
fpath = os.path.join(top, fn)
|
||||
annotation_lines = []
|
||||
with open(fpath, "r") as fd:
|
||||
i = 0
|
||||
for line in fd.readlines():
|
||||
i += 1
|
||||
if ANNOTATION_RE.search(line):
|
||||
tag, msg = ANNOTATION_RE.findall(line)[0]
|
||||
if options["tag"]:
|
||||
if tag not in map(
|
||||
str.upper, map(str, options["tag"])
|
||||
):
|
||||
break
|
||||
|
||||
if ANNOTATION_END_RE.search(msg.strip()):
|
||||
msg = ANNOTATION_END_RE.findall(msg.strip())[0][
|
||||
0
|
||||
]
|
||||
|
||||
annotation_lines.append(
|
||||
"[%3s] %-5s %s" % (i, tag, msg.strip())
|
||||
)
|
||||
if annotation_lines:
|
||||
self.stdout.write("%s:" % fpath)
|
||||
for annotation in annotation_lines:
|
||||
self.stdout.write(" * %s" % annotation)
|
||||
self.stdout.write("")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user