![PyPI Now Supports iOS and Android Wheels for Mobile Python Development](https://cdn.sanity.io/images/cgdhsj6q/production/96416c872705517a6a65ad9646ce3e7caef623a0-1024x1024.webp?w=400&fit=max&auto=format)
Security News
PyPI Now Supports iOS and Android Wheels for Mobile Python Development
PyPI now supports iOS and Android wheels, making it easier for Python developers to distribute mobile packages.
############################################################# 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.
::
pip install django-sorcery
Lets start by creating a site:
.. code:: console
$ django-admin startproject mysite
And lets create an app:
.. code:: console
$ cd mysite $ python manage.py startapp polls
This will create a polls app with standard django app layout:
.. code:: console
$ 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
:
.. code:: python
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
:
.. code:: python
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:
.. code:: console
$ 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
:
.. code:: python
""" 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 = '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:
.. code:: console
$ 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:
.. code:: console
$ python manage.py sorcery upgrade Running migrations for polls on database default
Right now, we have enough to hop in django shell:
.. code:: console
$ python manage.py shell
from polls.models import Choice, Question, db # Import the model classes and the db
Choice.query.all() [] Question.query.all() []
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=), question_text="What's new?")
db.add(q)
db.new IdentitySet([Question(pk=None, pub_date=datetime.datetime(2018, 5, 19, 0, 54, 20, 778186, tzinfo=), question_text="What's new?")])
db.flush()
q.pk 1
q.question_text = "What's up?" db.flush()
Question.objects <django_sorcery.db.query.Query at 0x7feb1c7899e8> Question.query <django_sorcery.db.query.Query at 0x7feb1c9377f0>
Question.objects.all() [Question(pk=1, pub_date=datetime.datetime(2018, 5, 19, 0, 54, 20, 778186, tzinfo=), question_text="What's up?")]
exit()
Let's add a couple of views in polls/views.py
, starting with a list view:
.. code:: python
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
:
.. code:: python
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:
.. code:: python
MIDDLEWARE = [ 'django_sorcery.db.middleware.SQLAlchemyMiddleware', # ... ]
and add some templates:
polls/templates/polls/index.html
:
.. code:: html
{% if latest_question_list %}
{% else %}No polls are available.
{% endif %}polls/templates/polls/detail.html
:
.. code:: html
{% if error_message %}
{{ error_message }}
{% endif %} {% csrf_token %} {% for choice in question.choices %} {{ choice.choice_text }}polls/templates/polls/results.html
:
.. code:: html
This is all fine but we can do one better using generic views. Lets adjust our views in polls/views.py
:
.. code:: python
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:
.. code:: python
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
:
.. code:: python
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:
.. code:: python
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:
.. code:: console
$ ./manage.py run show_urls /polls/ polls.views.PollsViewSet polls:question-list /polls// polls.views.PollsViewSet polls:question-detail /polls//delete/ polls.views.PollsViewSet polls:question-destroy /polls//edit/ polls.views.PollsViewSet polls:question-edit /polls//results/ polls.views.PollsViewSet polls:question-results /polls//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
:
.. code:: python
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
.. code:: html
... {{ choice_formset }} ..... |Build Status| image:: https://github.com/shosca/django-sorcery/workflows/Build/badge.svg?branch=master :target: https://github.com/shosca/django-sorcery/actions?query=workflow%3ABuild+branch%3Amaster .. |Read The Docs| image:: https://readthedocs.org/projects/django-sorcery/badge/?version=latest :target: http://django-sorcery.readthedocs.io/en/latest/?badge=latest .. |PyPI version| image:: https://badge.fury.io/py/django-sorcery.svg :target: https://badge.fury.io/py/django-sorcery .. |Coveralls Status| image:: https://coveralls.io/repos/github/shosca/django-sorcery/badge.svg?branch=master :target: https://coveralls.io/github/shosca/django-sorcery?branch=master .. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black
FAQs
Django and SQLAlchemy integration
We found that django-sorcery demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
PyPI now supports iOS and Android wheels, making it easier for Python developers to distribute mobile packages.
Security News
Create React App is officially deprecated due to React 19 issues and lack of maintenance—developers should switch to Vite or other modern alternatives.
Security News
Oracle seeks to dismiss fraud claims in the JavaScript trademark dispute, delaying the case and avoiding questions about its right to the name.