Package: django-sorcery
Free software: MIT licenseGitHub:https://github.com/shosca/django-sorcerySQLAlchemy 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.Installationpip install django-sorceryQuick StartLets start by creating a site:$django-adminstartprojectmysiteAnd lets create an app:$cdmysite$pythonmanage.pystartapppollsThis 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 filesAnd lets add ourpollsapp anddjango_sorceryinINSTALLED_APPSinmysite/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 withsqlalchemy. Lets define our models inpolls/models.py:fromdjango_sorcery.dbimportdatabasesdb=databases.get("default")classQuestion(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())classChoice(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 usingalembicintegration:$pythonmanage.pysorceryrevision-m"Add question and poll models"pollsGenerating ./polls/migrations/3983fc419e10_add_question_and_poll_models.py ... doneLet’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
"""fromalembicimportopimportsqlalchemyassa# revision identifiers, used by Alembic.revision='3983fc419e10'down_revision=Nonebranch_labels=Nonedepends_on=Nonedefupgrade():# ### 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 ###defdowngrade():# ### 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:$pythonmanage.pysorceryupgrade--sqlpollsCREATE 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:$pythonmanage.pysorceryupgradeRunning migrations for polls on database defaultRight now, we have enough to hop in django shell:$pythonmanage.pyshell>>> from polls.models import Choice, Question, db  # Import the model classes and the db#wehavenochoicesorquestionsindbyet>>> Choice.query.all()
[]
>>> Question.query.all()
[]#Letscreateanewquestion>>> 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?")#letssaveourquestion,weneedtoaddourquestiontothedb>>> db.add(q)#atthispointthequestionisinpendingstate>>> db.new
IdentitySet([Question(pk=None, pub_date=datetime.datetime(2018, 5, 19, 0, 54, 20, 778186, tzinfo=<UTC>), question_text="What's new?")])#letsflushtothedatabase>>> db.flush()#atthispointourquestionisinpersistentstateandwillreceiveaprimarykey>>> q.pk
1#letschangethequestiontext>>> q.question_text = "What's up?"
>>> db.flush()#Question.objectsandQuestion.queryarebothquerypropertiesthatreturnaqueryobjectboundtodb>>> Question.objects
<django_sorcery.db.query.Query at 0x7feb1c7899e8>
>>> Question.query
<django_sorcery.db.query.Query at 0x7feb1c9377f0>#andletsseeallthequestions>>> 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 inpolls/views.py, starting with a list view:fromdjango.shortcutsimportrenderfromdjango.templateimportloaderfromdjango.httpimportHttpResponseRedirectfromdjango.urlsimportreversefromdjango_sorcery.shortcutsimportget_object_or_404from.modelsimportQuestion,Choice,dbdefindex(request):latest_question_list=Question.objects.order_by(Question.pub_date.desc())[:5]context={'latest_question_list':latest_question_list}returnrender(request,'polls/index.html',context)defdetail(request,question_id):question=get_object_or_404(Question,pk=question_id)returnrender(request,'polls/detail.html',{'question':question})defresults(request,question_id):question=get_object_or_404(Question,pk=question_id)returnrender(request,'polls/results.html',{'question':question})defvote(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()ifnotselected_choice:returnrender(request,'polls/detail.html',{'question':question,'error_message':"You didn't select a choice.",})selected_choice.votes+=1db.flush()returnHttpResponseRedirect(reverse('polls:results',args=(question.pk,)))and register the view inpolls/urls.py:fromdjango.urlsimportpathfrom.importviewsapp_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 theSQLAlchemyMiddlewareto 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><ahref="{% 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 %}<formaction="{% url 'polls:vote' question.pk %}"method="post">{% csrf_token %}
{% for choice in question.choices %}<inputtype="radio"name="choice"id="choice{{ forloop.counter }}"value="{{ choice.pk }}"/><labelfor="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br/>{% endfor %}<inputtype="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><ahref="{% 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 inpolls/views.py:fromdjango.shortcutsimportrenderfromdjango.httpimportHttpResponseRedirectfromdjango.urlsimportreversefromdjango_sorcery.shortcutsimportget_object_or_404fromdjango_sorceryimportviewsfrom.modelsimportQuestion,Choice,dbclassIndexView(views.ListView):template_name='polls/index.html'context_object_name='latest_question_list'defget_queryset(self):returnQuestion.objects.order_by(Question.pub_date.desc())[:5]classDetailView(views.DetailView):model=Questionsession=dbtemplate_name='polls/detail.html'classResultsView(DetailView):template_name='polls/results.html'defvote(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()ifnotselected_choice:returnrender(request,'polls/detail.html',{'question':question,'error_message':"You didn't select a choice.",})selected_choice.votes+=1db.flush()returnHttpResponseRedirect(reverse('polls:results',args=(question.pk,)))and adjust thepolls/urls.pylike:fromdjango.urlsimportpathfrom.importviewsapp_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 fortemplate_nameandcontext_object_nameare similar to django’s generic views. If we
handn’t defined those the default for template names would’ve beenpolls/question_detail.htmlandpolls/question_list.htmlfor the detail and list template names, andquestionandquestion_listfor 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 inpolls/views.py:fromdjango.httpimportHttpResponseRedirectfromdjango.urlsimportreverse,reverse_lazyfromdjango_sorcery.routersimportactionfromdjango_sorcery.viewsetsimportModelViewSetfrom.modelsimportQuestion,Choice,dbclassPollsViewSet(ModelViewSet):model=Questionfields="__all__"destroy_success_url=reverse_lazy("polls:question-list")defget_success_url(self):returnreverse("polls:question-detail",kwargs={"pk":self.object.pk})@action(detail=True)defresults(self,request,*args,**kwargs):returnself.retrieve(request,*args,**kwargs)@action(detail=True,methods=["POST"])defvote(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()ifnotselected_choice:context=self.get_detail_context_data(object=self.object)context["error_message"]="You didn't select a choice."self.action="retrieve"returnself.render_to_response(context)selected_choice.votes+=1db.flush()returnHttpResponseRedirect(reverse("polls:question-results",args=(self.object.pk,)))And adjusting ourpolls/urls.pylike:fromdjango.urlsimportpath,includefromdjango_sorcery.routersimportSimpleRouterfrom.importviewsrouter=SimpleRouter()router.register("",views.PollsViewSet)app_name="polls"urlpatterns=[path("",include(router.urls))]With these changes we’ll have the following urls:$./manage.pyrunshow_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-newThis will map the following operations to following actions on the viewset:MethodPathActionRoute NameGET/polls/listquestion-listPOST/polls/createquestion-listGET/polls/new/newquestion-newGET/polls/1/retrievequestion-detailPOST/polls/1/updatequestion-detailPUT/polls/1/updatequestion-detailPATCH/polls/1/updatequestion-detailDELETE/polls/1/destroyquestion-detailGET/polls/1/edit/editquestion-editGET/polls/1/delete/confirm_destoyquestion-deletePOST/polls/1/delete/destroyquestion-deleteNow, lets add an inline formset to be able to add choices to questions, adjustpolls/views.py:fromdjango.httpimportHttpResponseRedirectfromdjango.urlsimportreverse,reverse_lazyfromdjango_sorcery.routersimportactionfromdjango_sorcery.viewsetsimportModelViewSetfromdjango_sorcery.formsetsimportinlineformset_factoryfrom.modelsimportQuestion,Choice,dbChoiceFormSet=inlineformset_factory(relation=Question.choices,fields=(Choice.choice_text.key,),session=db)classPollsViewSet(ModelViewSet):model=Questionfields=(Question.question_text.key,Question.pub_date.key)destroy_success_url=reverse_lazy("polls:question-list")defget_success_url(self):returnreverse("polls:question-detail",kwargs={"pk":self.object.pk})defget_form_context_data(self,**kwargs):kwargs["choice_formset"]=self.get_choice_formset()returnsuper().get_form_context_data(**kwargs)defget_choice_formset(self,instance=None):ifnothasattr(self,"_choice_formset"):instance=instanceorself.objectself._choice_formset=ChoiceFormSet(instance=instance,data=self.request.POSTifself.request.POSTelseNone)returnself._choice_formsetdefprocess_form(self,form):ifform.is_valid()andself.get_choice_formset(instance=form.instance).is_valid():returnself.form_valid(form)returnform.invalid(self,form)defform_valid(self,form):self.object=form.save()self.object.choices=self.get_choice_formset().save()db.flush()returnHttpResponseRedirect(self.get_success_url())@action(detail=True)defresults(self,request,*args,**kwargs):returnself.retrieve(request,*args,**kwargs)@action(detail=True,methods=["POST"])defvote(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()ifnotselected_choice:context=self.get_detail_context_data(object=self.object)context["error_message"]="You didn't select a choice."self.action="retrieve"returnself.render_to_response(context)selected_choice.votes+=1db.flush()returnHttpResponseRedirect(reverse("polls:question-results",args=(self.object.pk,)))And addchoice_formsetin thepolls/templates/question_edit.htmlandpolls/templates/question_edit.html<form...>...
   {{ choice_formset }}
   ...</form>