new models, frontend functions, public pages

This commit is contained in:
2025-05-07 15:41:03 +09:00
parent 91f0d54563
commit 18497d4343
784 changed files with 124024 additions and 289 deletions

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from django.utils.version import get_version
VERSION = (4, 1, 0, "final", 0)
__version__ = get_version(VERSION)

View File

@@ -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

View File

@@ -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")),
)

View File

@@ -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 = "?" + "&amp;".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))

View File

@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from django.apps import AppConfig
class DjangoExtensionsConfig(AppConfig):
name = "django_extensions"
verbose_name = "Django Extensions"

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -0,0 +1,3 @@
from django import forms
# place form definition here

View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@@ -0,0 +1,3 @@
from django.urls import include, path
# place app url patterns here

View File

@@ -0,0 +1 @@
# Create your views here.

View File

@@ -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()

View File

@@ -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

View File

@@ -0,0 +1,3 @@
from django import template
register = template.Library()

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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]

View File

@@ -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

View File

@@ -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")

View File

@@ -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 "

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 "Αναζήτηση"

View File

@@ -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 ""

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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."

View File

@@ -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"

View File

@@ -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 "検索"

View File

@@ -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."

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 "Поиск"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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__())

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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)
)

View File

@@ -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),
}

View File

@@ -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.")

View File

@@ -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

View File

@@ -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())

View File

@@ -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)))

View File

@@ -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 Pythons 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))

View File

@@ -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()

View File

@@ -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_)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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