New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

django-sorcery

Package Overview
Dependencies
Maintainers
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

django-sorcery

Django and SQLAlchemy integration

  • 0.13.0
  • Source
  • PyPI
  • Socket score

Maintainers
1

############################################################# Django Sorcery - Django Framework integration with SQLAlchemy #############################################################

|Build Status| |Read The Docs| |PyPI version| |Coveralls Status| |Black|

  • Free software: MIT license
  • GitHub: https://github.com/shosca/django-sorcery

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:

.. 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 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:

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

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=), 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=), 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=), 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

{{ question.question_text }}

{% if error_message %}

{{ error_message }}

{% endif %}

{% csrf_token %} {% for choice in question.choices %} {{ choice.choice_text }}
{% endfor %}

polls/templates/polls/results.html:

.. code:: html

{{ question.question_text }}

  • {{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}

Vote again?

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

Keywords

FAQs


Did you know?

Socket

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.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc