Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ docs/_build
# IDEs
/.idea
.venv
.envrc
.pytest_cache
db.sqlite3
.mypy_cache
Expand Down
2 changes: 2 additions & 0 deletions django_sorcery/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
32 changes: 32 additions & 0 deletions django_sorcery/admin/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
from functools import wraps

from django.contrib.admin import options
from django.contrib.admin.options import ModelAdmin as DjangoModelAdmin

from ..forms import ModelForm, modelform_factory


def swap(module, attr, replacement):
def wrapper(f):
@wraps(f)
def inner(*args, **kwargs):
original = getattr(module, attr)
setattr(module, attr, replacement)
try:
return f(*args, **kwargs)
finally:
setattr(module, attr, original)

return inner

return wrapper


class ModelAdmin(DjangoModelAdmin):
form = ModelForm

@swap(options, "modelform_factory", modelform_factory)
def get_form(self, *args, **kwargs):
return super(ModelAdmin, self).get_form(*args, **kwargs)
5 changes: 5 additions & 0 deletions django_sorcery/db/meta/column.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ def required(self):
def name(self):
return self.property.key if self.property is not None else self.column.key

attname = name

@property
def parent_model(self):
return self.property.parent.class_ if self.property else None
Expand Down Expand Up @@ -146,6 +148,9 @@ def field_kwargs(self):

return kwargs

def to_python(self, value):
return self.formfield().clean(value)


class string_column_info(column_info):
default_form_class = djangofields.CharField
Expand Down
4 changes: 4 additions & 0 deletions django_sorcery/db/meta/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ class relation_info(object):
def __init__(self, relationship):
self.relationship = relationship

@property
def choices(self):
return True

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:trollface:


@property
def attribute(self):
return getattr(self.parent_model, self.name)
Expand Down
56 changes: 55 additions & 1 deletion django_sorcery/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import sqlalchemy.ext.declarative # noqa
import sqlalchemy.orm # noqa
from sqlalchemy.orm.base import MANYTOONE, NO_VALUE
from sqlalchemy.orm.exc import NoResultFound

from django.apps import apps
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.forms.fields import DateField
from django.utils.text import camel_case_to_spaces

Expand Down Expand Up @@ -268,6 +270,46 @@ def clone(instance, *rels, **kwargs):
return cloned


class Options(object):
def __init__(self, model, options, app_label=None):
self.model = model
self.meta = options

self.apps = apps
self.app_label = app_label or apps.get_containing_app_config(self.model.__module__).label

self.db_table = model.__tablename__
self.pk = next(iter(meta.model_info(model).primary_keys.values()))

self.object_name = model.__name__
self.model_name = self.object_name.lower()
self.verbose_name = getattr(meta, "verbose_name", camel_case_to_spaces(self.object_name))
self.verbose_name_plural = getattr(meta, "verbose_name_plural", "{}s".format(self.verbose_name))

self.abstract = False
self.swapped = False
self.ordering = []

@property
def app_config(self):
# Don't go through get_app_config to avoid triggering imports.
return self.apps.app_configs.get(self.app_label)

def get_field(self, name):
try:
return dict(meta.model_info(self.model).column_properties)[name]
except KeyError:
raise FieldDoesNotExist

def __repr__(self):
return "<{}({})>".format(self.__class__.__name__, self.object_name)


class OptionsDescriptor(object):
def __get__(self, instance, owner):
return Options(owner, getattr(owner, "Meta", None))


class BaseMeta(sqlalchemy.ext.declarative.DeclarativeMeta):
"""
Base metaclass for models which registers models to DB model registry
Expand All @@ -277,6 +319,7 @@ class BaseMeta(sqlalchemy.ext.declarative.DeclarativeMeta):
def __new__(typ, name, bases, attrs):
klass = super(BaseMeta, typ).__new__(typ, name, bases, attrs)
typ.db.models_registry.append(klass)
klass.session = typ.db
return klass


Expand Down Expand Up @@ -343,6 +386,17 @@ def _get_relation_objects_for_validation(self):
"""
return meta.model_info(self.__class__).relationships

# ----------
# Django API
# ----------

_meta = OptionsDescriptor()

DoesNotExist = NoResultFound

def serializable_value(self, field_name):
return getattr(self, field_name)


_instant_defaults = set()

Expand Down
36 changes: 33 additions & 3 deletions django_sorcery/db/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,29 @@
}


class DjangoCompiledQuery(object):
def __init__(self, query):
self.query = query
self.order_by = []
self.select_related = True


class Query(sa.orm.Query):
"""
A customized sqlalchemy query
"""

@property
def model(self):
return self._only_full_mapper_zero("get").class_

def get(self, *args, **kwargs):
"""
Return an instance based on the given primary key identifier, either as args or
kwargs for composite keys. If no instance is found, returns ``None``.
"""
if kwargs:
mapper = self._only_full_mapper_zero("get")
pk = meta.model_info(mapper).primary_keys_from_dict(kwargs)
pk = meta.model_info(self.model).primary_keys_from_dict(kwargs)

if pk is not None:
return super(Query, self).get(pk)
Expand All @@ -83,9 +93,22 @@ def filter(self, *args, **kwargs):
args = args + tuple(self._lookup_to_expression(k, v) for k, v in kwargs.items())
return super(Query, self).filter(*args)

def get_queryset(self):
"""
Necessary to comply with Django API
"""
return self

@property
def query(self):
"""
Necessary to comply with Django API
"""
return DjangoCompiledQuery(self)

def _lookup_to_expression(self, lookup, value):
parts = lookup.split(LOOKUP_SEP)
info = meta.model_info(self._only_full_mapper_zero("get"))
info = meta.model_info(self.model)

props = dict(info.column_properties)
lhs = None
Expand Down Expand Up @@ -121,6 +144,13 @@ def _lookup_to_expression(self, lookup, value):

return lhs == value

# todo need to make len(query) == len(cl.result_list.all()) without recursion
# currently requires patch in django/contrib/admin/options.py
# 1792 - 'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(cl.result_list)},
# 1792 + 'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(cl.result_list.all())},
# def __len__(self):
# return self.count()

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, super().count()?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just tried. nope. still recursion

sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) parser stack overflow [SQL: 'SELECT count(*) AS count_1 \nFROM (SELECT count(*) AS count_2 \nFROM (SELECT count(*) AS count_3 \nFROM (SELECT count(*) AS count_4 \nFROM (SELECT count(*) AS count_5 \nFROM (SELECT count(*) AS count_6 \nFROM (SELECT count(*) AS count_7 \nFROM (SELECT count(*) AS count_8 \nFROM (SELECT count(*) AS count_9 \nFROM (SELECT count(*) AS count_10 \nFROM (SELECT count(*) AS count_11 \nFROM (SELECT count(*) AS count_12 \nFROM (SELECT count(*) AS count_13 \nFROM (SELECT count(*) AS count_14 \nFROM (SELECT count(*) AS count_15 \nFROM (SELECT question.pk AS question_pk, question.question_text AS question_question_text, question.pub_date AS question_pub_date \nFROM question ORDER BY -pk) AS anon_15) AS anon_14) AS anon_13) AS anon_12) AS anon_11) AS anon_10) AS anon_9) AS anon_8) AS anon_7) AS anon_6) AS anon_5) AS anon_4) AS anon_3) AS anon_2) AS anon_1'] (Background on this error at: http://sqlalche.me/e/e3q8)



class QueryProperty(object):
def __init__(self, db, model=None, *args, **kwargs):
Expand Down
7 changes: 5 additions & 2 deletions django_sorcery/db/sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,13 @@ def _make_declarative(self, model):
# allow to customize things in custom base model
if not hasattr(base, "query_class"):
base.query_class = self.query_class
prop = self.queryproperty()
if not hasattr(base, "query"):
base.query = self.queryproperty()
base.query = prop
if not hasattr(base, "objects"):
base.objects = self.queryproperty()
base.objects = prop
if not hasattr(base, "_default_manager"):
base._default_manager = prop

return base

Expand Down
8 changes: 4 additions & 4 deletions django_sorcery/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
import six

from django.core.exceptions import NON_FIELD_ERRORS, ImproperlyConfigured, ValidationError
from django.forms import ALL_FIELDS
from django.forms.forms import BaseForm, DeclarativeFieldsMetaclass
from django.forms import ALL_FIELDS, BaseModelForm as DjangoBaseModelForm
from django.forms.forms import DeclarativeFieldsMetaclass
from django.forms.models import ModelFormOptions
from django.forms.utils import ErrorList

Expand Down Expand Up @@ -240,7 +240,7 @@ def __new__(cls, name, bases, attrs):
return mcs


class BaseModelForm(BaseForm):
class BaseModelForm(DjangoBaseModelForm):
def __init__(
self,
data=None,
Expand All @@ -257,10 +257,10 @@ def __init__(
session=None,
):
opts = self._meta
opts.session = opts.session or session
if opts.model is None:
raise ValueError("ModelForm has no model class specified.")

opts.session = opts.session or session or getattr(opts.model, "session", None)
if opts.session is None:
raise ValueError("ModelForm has no session specified.")

Expand Down
25 changes: 14 additions & 11 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
-e .
pytest
pytest-cov
attrs
beautifulsoup4
coveralls
django-extensions
flake8
simplejson
sphinx-autobuild
gitchangelog
yapf
mock; python_version <= '2.7'
pdbpp
pre_commit
psycopg2
ptpython
pytest
pytest-cov
simplejson
Sphinx
sphinx-autobuild
sphinx_rtd_theme
mock; python_version <= '2.7'
beautifulsoup4
attrs
tox
coveralls
sqlalchemy_utils
psycopg2
tox
Werkzeug
yapf
4 changes: 3 additions & 1 deletion test_site/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import sys


sys.path.insert(0, "..")
sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__))))
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))


if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_site.settings")
Expand Down
15 changes: 13 additions & 2 deletions test_site/polls/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals

from django.contrib import admin # noqa
from django.contrib import admin

from django_sorcery.admin.options import ModelAdmin

# Register your models here.
from .models import Choice, Question


@admin.register(Question)
class QuestionAdmin(ModelAdmin):
pass


@admin.register(Choice)
class ChoiceAdmin(ModelAdmin):
pass
6 changes: 6 additions & 0 deletions test_site/polls/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class Question(db.Model):
question_text = db.Column(db.String(length=200))
pub_date = db.Column(db.DateTime())

def __str__(self):
return self.question_text


class Choice(db.Model):
pk = db.Column(db.Integer(), autoincrement=True, primary_key=True)
Expand All @@ -20,6 +23,9 @@ class Choice(db.Model):

question = db.ManyToOne(Question, backref=db.backref("choices", cascade="all, delete-orphan"))

def __str__(self):
return self.choice_text


db.configure_mappers()
db.create_all()