Code Monkey home page Code Monkey logo

django-sorcery's Introduction

Django Sorcery - Django Framework integration with SQLAlchemy

Build Status Read The Docs PyPI version Coveralls Status Black

SQLAlchemy is an excellent orm. And Django is a great framework, until you decide not to use Django ORM. This library provides utilities, helpers and configurations to ease the pain of using SQLAlchemy with Django. It aims to provide a similar development experience to building a Django application with Django ORM, except with SQLAlchemy.

Installation

pip install django-sorcery

Quick Start

Lets start by creating a site:

$ django-admin startproject mysite

And lets create an app:

$ cd mysite
$ python manage.py startapp polls

This will create a polls app with standard django app layout:

$ tree
.
├── manage.py
├── polls
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
└── mysite
   ├── __init__.py
   ├── settings.py
   ├── urls.py
   └── wsgi.py

3 directories, 12 files

And lets add our polls app and django_sorcery in INSTALLED_APPS in mysite/settings.py:

INSTALLED_APPS = [
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'django_sorcery',
   'polls.apps.PollsConfig',
]

Now we're going to make a twist and start building our app with sqlalchemy. Lets define our models in polls/models.py:

from django_sorcery.db import databases


db = databases.get("default")


class Question(db.Model):
   pk = db.Column(db.Integer(), autoincrement=True, primary_key=True)
   question_text = db.Column(db.String(length=200))
   pub_date = db.Column(db.DateTime())


class Choice(db.Model):
   pk = db.Column(db.Integer(), autoincrement=True, primary_key=True)
   choice_text = db.Column(db.String(length=200))
   votes = db.Column(db.Integer(), default=0)

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

Now that we have some models, lets create a migration using alembic integration:

$ python manage.py sorcery revision -m "Add question and poll models" polls
  Generating ./polls/migrations/3983fc419e10_add_question_and_poll_models.py ... done

Let's take a look at the generated migration file ./polls/migrations/3983fc419e10_add_question_and_poll_models.py:

"""
Add question and poll models

Revision ID: 3983fc419e10
Revises:
Create Date: 2019-04-16 20:57:48.154179
"""

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '3983fc419e10'
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
   # ### commands auto generated by Alembic - please adjust! ###
   op.create_table('question',
   sa.Column('pk', sa.Integer(), autoincrement=True, nullable=False),
   sa.Column('question_text', sa.String(length=200), nullable=True),
   sa.Column('pub_date', sa.DateTime(), nullable=True),
   sa.PrimaryKeyConstraint('pk')
   )
   op.create_table('choice',
   sa.Column('pk', sa.Integer(), autoincrement=True, nullable=False),
   sa.Column('choice_text', sa.String(length=200), nullable=True),
   sa.Column('votes', sa.Integer(), nullable=True),
   sa.Column('question_pk', sa.Integer(), nullable=True),
   sa.ForeignKeyConstraint(['question_pk'], ['question.pk'], ),
   sa.PrimaryKeyConstraint('pk')
   )
   # ### end Alembic commands ###


def downgrade():
   # ### commands auto generated by Alembic - please adjust! ###
   op.drop_table('choice')
   op.drop_table('question')
   # ### end Alembic commands ###

Let's take a look at generated sql:

$ python manage.py sorcery upgrade --sql polls

CREATE TABLE alembic_version_polls (
   version_num VARCHAR(32) NOT NULL,
   CONSTRAINT alembic_version_polls_pkc PRIMARY KEY (version_num)
);

-- Running upgrade  -> d7d86e07cc8e

CREATE TABLE question (
   pk INTEGER NOT NULL,
   question_text VARCHAR(200),
   pub_date DATETIME,
   PRIMARY KEY (pk)
);

CREATE TABLE choice (
   pk INTEGER NOT NULL,
   choice_text VARCHAR(200),
   votes INTEGER,
   question_pk INTEGER,
   PRIMARY KEY (pk),
   FOREIGN KEY(question_pk) REFERENCES question (pk)
);

INSERT INTO alembic_version_polls (version_num) VALUES ('d7d86e07cc8e');

Let's bring our db up to date:

$ python manage.py sorcery upgrade
Running migrations for polls on database default

Right now, we have enough to hop in django shell:

$ python manage.py shell

>>> from polls.models import Choice, Question, db  # Import the model classes and the db

# we have no choices or questions in db yet
>>> Choice.query.all()
[]
>>> Question.query.all()
[]

# Lets create a new question
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())
>>> q
Question(pk=None, pub_date=datetime.datetime(2018, 5, 19, 0, 54, 20, 778186, tzinfo=<UTC>), question_text="What's new?")

# lets save our question, we need to add our question to the db
>>> db.add(q)

# at this point the question is in pending state
>>> db.new
IdentitySet([Question(pk=None, pub_date=datetime.datetime(2018, 5, 19, 0, 54, 20, 778186, tzinfo=<UTC>), question_text="What's new?")])

# lets flush to the database
>>> db.flush()

# at this point our question is in persistent state and will receive a primary key
>>> q.pk
1

# lets change the question text
>>> q.question_text = "What's up?"
>>> db.flush()

# Question.objects and Question.query are both query properties that return a query object bound to db
>>> Question.objects
<django_sorcery.db.query.Query at 0x7feb1c7899e8>
>>> Question.query
<django_sorcery.db.query.Query at 0x7feb1c9377f0>

# and lets see all the questions
>>> Question.objects.all()
[Question(pk=1, pub_date=datetime.datetime(2018, 5, 19, 0, 54, 20, 778186, tzinfo=<UTC>), question_text="What's up?")]

>>> exit()

Let's add a couple of views in polls/views.py, starting with a list view:

from django.shortcuts import render
from django.template import loader
from django.http import HttpResponseRedirect
from django.urls import reverse

from django_sorcery.shortcuts import get_object_or_404

from .models import Question, Choice, db

def index(request):
   latest_question_list = Question.objects.order_by(Question.pub_date.desc())[:5]
   context = {'latest_question_list': latest_question_list}
   return render(request, 'polls/index.html', context)


def detail(request, question_id):
   question = get_object_or_404(Question, pk=question_id)
   return render(request, 'polls/detail.html', {'question': question})


def results(request, question_id):
   question = get_object_or_404(Question, pk=question_id)
   return render(request, 'polls/results.html', {'question': question})


def vote(request, question_id):
   question = get_object_or_404(Question, pk=question_id)

   selected_choice = Choice.query.filter(
      Choice.question == question,
      Choice.pk == request.POST['choice'],
   ).one_or_none()

   if not selected_choice:
      return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
      })

   selected_choice.votes += 1
   db.flush()
   return HttpResponseRedirect(reverse('polls:results', args=(question.pk,)))

and register the view in polls/urls.py:

from django.urls import path

from . import views


app_name = 'polls'
urlpatterns = [
   path('', views.index, name='index'),
   path('<int:question_id>/', views.detail, name='detail'),
   path('<int:question_id>/results', views.results, name='results'),
   path('<int:question_id>/vote', views.vote, name='vote'),
]

and register the SQLAlchemyMiddleware to provide unit-of-work per request pattern:

MIDDLEWARE = [
   'django_sorcery.db.middleware.SQLAlchemyMiddleware',
   # ...
]

and add some templates:

polls/templates/polls/index.html:

{% if latest_question_list %}
<ul>
{% for question in latest_question_list %}
<li><a href="{% url 'polls:detail' question.pk %}">{{ question.question_text }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}

polls/templates/polls/detail.html:

<h1>{{ question.question_text }}</h1>

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="{% url 'polls:vote' question.pk %}" method="post">
{% csrf_token %}
{% for choice in question.choices %}
   <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.pk }}" />
   <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
{% endfor %}
<input type="submit" value="Vote" />
</form>

polls/templates/polls/results.html:

<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choices %}
   <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.pk %}">Vote again?</a>

This is all fine but we can do one better using generic views. Lets adjust our views in polls/views.py:

from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse

from django_sorcery.shortcuts import get_object_or_404
from django_sorcery import views

from .models import Question, Choice, db


class IndexView(views.ListView):
   template_name = 'polls/index.html'
   context_object_name = 'latest_question_list'

   def get_queryset(self):
      return Question.objects.order_by(Question.pub_date.desc())[:5]


class DetailView(views.DetailView):
   model = Question
   session = db
   template_name = 'polls/detail.html'


class ResultsView(DetailView):
   template_name = 'polls/results.html'


def vote(request, question_id):
   question = get_object_or_404(Question, pk=question_id)

   selected_choice = Choice.query.filter(
      Choice.question == question,
      Choice.pk == request.POST['choice'],
   ).one_or_none()

   if not selected_choice:
      return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
      })

   selected_choice.votes += 1
   db.flush()
   return HttpResponseRedirect(reverse('polls:results', args=(question.pk,)))

and adjust the polls/urls.py like:

from django.urls import path

from . import views


app_name = 'polls'
urlpatterns = [
   path('', views.IndexView.as_view(), name='index'),
   path('<int:pk>/', views.DetailView.as_view(), name='detail'),
   path('<int:pk>/results', views.ResultsView.as_view(), name='results'),
   path('<int:question_id>/vote', views.vote, name='vote'),
]

The default values for template_name and context_object_name are similar to django's generic views. If we handn't defined those the default for template names would've been polls/question_detail.html and polls/question_list.html for the detail and list template names, and question and question_list for context names for detail and list views.

This is all fine but we can even do one better using a viewset. Lets adjust our views in polls/views.py:

from django.http import HttpResponseRedirect
from django.urls import reverse, reverse_lazy

from django_sorcery.routers import action
from django_sorcery.viewsets import ModelViewSet

from .models import Question, Choice, db


class PollsViewSet(ModelViewSet):
   model = Question
   fields = "__all__"
   destroy_success_url = reverse_lazy("polls:question-list")

   def get_success_url(self):
      return reverse("polls:question-detail", kwargs={"pk": self.object.pk})

   @action(detail=True)
   def results(self, request, *args, **kwargs):
      return self.retrieve(request, *args, **kwargs)

   @action(detail=True, methods=["POST"])
   def vote(self, request, *args, **kwargs):
      self.object = self.get_object()

      selected_choice = Choice.query.filter(
            Choice.question == self.object, Choice.pk == request.POST.get("choice")
      ).one_or_none()

      if not selected_choice:
            context = self.get_detail_context_data(object=self.object)
            context["error_message"] = "You didn't select a choice."
            self.action = "retrieve"
            return self.render_to_response(context)

      selected_choice.votes += 1
      db.flush()
      return HttpResponseRedirect(reverse("polls:question-results", args=(self.object.pk,)))

And adjusting our polls/urls.py like:

from django.urls import path, include

from django_sorcery.routers import SimpleRouter

from . import views

router = SimpleRouter()
router.register("", views.PollsViewSet)

app_name = "polls"
urlpatterns = [path("", include(router.urls))]

With these changes we'll have the following urls:

$ ./manage.py run show_urls
/polls/      polls.views.PollsViewSet        polls:question-list
/polls/<pk>/ polls.views.PollsViewSet        polls:question-detail
/polls/<pk>/delete/  polls.views.PollsViewSet        polls:question-destroy
/polls/<pk>/edit/    polls.views.PollsViewSet        polls:question-edit
/polls/<pk>/results/ polls.views.PollsViewSet        polls:question-results
/polls/<pk>/vote/    polls.views.PollsViewSet        polls:question-vote
/polls/new/  polls.views.PollsViewSet        polls:question-new

This will map the following operations to following actions on the viewset:

Method Path Action Route Name
GET /polls/ list question-list
POST /polls/ create question-list
GET /polls/new/ new question-new
GET /polls/1/ retrieve question-detail
POST /polls/1/ update question-detail
PUT /polls/1/ update question-detail
PATCH /polls/1/ update question-detail
DELETE /polls/1/ destroy question-detail
GET /polls/1/edit/ edit question-edit
GET /polls/1/delete/ confirm_destoy question-delete
POST /polls/1/delete/ destroy question-delete

Now, lets add an inline formset to be able to add choices to questions, adjust polls/views.py:

from django.http import HttpResponseRedirect
from django.urls import reverse, reverse_lazy

from django_sorcery.routers import action
from django_sorcery.viewsets import ModelViewSet
from django_sorcery.formsets import inlineformset_factory

from .models import Question, Choice, db


ChoiceFormSet = inlineformset_factory(relation=Question.choices, fields=(Choice.choice_text.key,), session=db)


class PollsViewSet(ModelViewSet):
   model = Question
   fields = (Question.question_text.key, Question.pub_date.key)
   destroy_success_url = reverse_lazy("polls:question-list")

   def get_success_url(self):
      return reverse("polls:question-detail", kwargs={"pk": self.object.pk})

   def get_form_context_data(self, **kwargs):
      kwargs["choice_formset"] = self.get_choice_formset()
      return super().get_form_context_data(**kwargs)

   def get_choice_formset(self, instance=None):
      if not hasattr(self, "_choice_formset"):
            instance = instance or self.object
            self._choice_formset = ChoiceFormSet(
               instance=instance, data=self.request.POST if self.request.POST else None
            )

      return self._choice_formset

   def process_form(self, form):
      if form.is_valid() and self.get_choice_formset(instance=form.instance).is_valid():
            return self.form_valid(form)

      return form.invalid(self, form)

   def form_valid(self, form):
      self.object = form.save()
      self.object.choices = self.get_choice_formset().save()
      db.flush()
      return HttpResponseRedirect(self.get_success_url())

   @action(detail=True)
   def results(self, request, *args, **kwargs):
      return self.retrieve(request, *args, **kwargs)

   @action(detail=True, methods=["POST"])
   def vote(self, request, *args, **kwargs):
      self.object = self.get_object()

      selected_choice = Choice.query.filter(
            Choice.question == self.object, Choice.pk == request.POST.get("choice")
      ).one_or_none()

      if not selected_choice:
            context = self.get_detail_context_data(object=self.object)
            context["error_message"] = "You didn't select a choice."
            self.action = "retrieve"
            return self.render_to_response(context)

      selected_choice.votes += 1
      db.flush()
      return HttpResponseRedirect(reverse("polls:question-results", args=(self.object.pk,)))

And add choice_formset in the polls/templates/question_edit.html and polls/templates/question_edit.html

<form ... >
   ...
   {{ choice_formset }}
   ...
</form >

django-sorcery's People

Contributors

asottile avatar miki725 avatar pre-commit-ci[bot] avatar shosca avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

django-sorcery's Issues

Initial Update

The bot created this issue to inform you that pyup.io has been set up on this repo.
Once you have closed it, the bot will open pull requests for updates as soon as they are available.

Error in signals.py

In the file signals.py this line from blinker._utilities import defaultdict is failing. Should the defaultdict be imported from the collections module in python?

Change use of force_text to force_str

Hi @shosca and everyone!

I'm currently using django-rest-witchcraft in my Django 4.0.3 project and there exists a problem when trying to run Django server:

  File "[route-to-venv]/lib/python3.9/site-packages/rest_witchcraft/serializers.py", line 13, in <module>
    from django_sorcery.db import meta
  File "[route-to-venv]/lib/python3.9/site-packages/django_sorcery/db/__init__.py", line 141, in <module>
    from .sqlalchemy import SQLAlchemy  # noqa
  File "[route-to-venv]/lib/python3.9/site-packages/django_sorcery/db/sqlalchemy.py", line 11, in <module>
    from . import fields, signals
  File "[route-to-venv]/lib/python3.9/site-packages/django_sorcery/db/fields.py", line 11, in <module>
    from .url import DIALECT_MAP_TO_DJANGO
  File "[route-to-venv]/lib/python3.9/site-packages/django_sorcery/db/url.py", line 7, in <module>
    from django.utils.encoding import force_text
ImportError: cannot import name 'force_text' from 'django.utils.encoding' ([route-to-venv]/lib/python3.9/site-packages/django/utils/encoding.py)

I looked for a fix for this bug through Internet and I found that it's as easy as changing uses of force_text by force_str and it all works properly! I created a PR (#168) and I'm looking forward to you all to check it and, if you think it works nicely, approve and incorporate it.

Thanks in advance!

Alembic migrations in "app/migrations" folder cause django development server to fail

After setting up a django app and creating a migration using sorcery revision command, I get the following error when starting django dev server:

Exception in thread django-main-thread:
Traceback (most recent call last):
  File "C:\Users\mi\AppData\Local\Programs\Python\Python37\lib\threading.py", line 917, in _bootstrap_inner
    self.run()
  File "C:\Users\mi\AppData\Local\Programs\Python\Python37\lib\threading.py", line 865, in run
    self._target(*self._args, **self._kwargs)
  File "C:\Users\mi\AppData\Local\Programs\Python\Python37\lib\site-packages\django\utils\autoreload.py", line 54, in wrapper
    fn(*args, **kwargs)
  File "C:\Users\mi\AppData\Local\Programs\Python\Python37\lib\site-packages\django\core\management\commands\runserver.py", line 120, in inner_run
    self.check_migrations()
  File "C:\Users\mi\AppData\Local\Programs\Python\Python37\lib\site-packages\django\core\management\base.py", line 453, in check_migrations
    executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS])
  File "C:\Users\mi\AppData\Local\Programs\Python\Python37\lib\site-packages\django\db\migrations\executor.py", line 18, in __init__
    self.loader = MigrationLoader(self.connection)
  File "C:\Users\mi\AppData\Local\Programs\Python\Python37\lib\site-packages\django\db\migrations\loader.py", line 49, in __init__
    self.build_graph()
  File "C:\Users\mi\AppData\Local\Programs\Python\Python37\lib\site-packages\django\db\migrations\loader.py", line 206, in build_graph
    self.load_disk()
  File "C:\Users\mi\AppData\Local\Programs\Python\Python37\lib\site-packages\django\db\migrations\loader.py", line 119, in load_disk
    "Migration %s in app %s has no Migration class" % (migration_name, app_config.label)
django.db.migrations.exceptions.BadMigrationError: Migration 0001_test in app fares has no Migration class

It appears that the migration check wants all the files in migrations folders in apps to be valid django migrations. For now I work around this by manually adding a dummy django migration to alembic migration file:

from django.db import migrations


class Migration(migrations.Migration):
    dependencies = [
    ]

    operations = [
    ]

One alternative would be using a different folder, perhaps alembic_migrations or revisions to avoid confusion with django ORM migration system.

sqlalchemy classical mapping

Hey folks,

I'm using sqlalchemy's datamapper approach through what they call the classical mapping

is it possible to implement this pattern using this package?

Sqlalchemy 2.0.0

Does anyone have ideas on what to do with Sqlalchemy 2 support? Will it be available?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.