django-sorcery


Namedjango-sorcery JSON
Version 0.13.0 PyPI version JSON
download
home_pagehttps://github.com/shosca/django-sorcery
SummaryDjango and SQLAlchemy integration
upload_time2023-04-11 11:20:16
maintainer
docs_urlNone
authorSerkan Hosca
requires_python
licenseMIT
keywords sqlalchemy django framework forms
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            #############################################################
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=<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:

.. 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 %}
   <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``:

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

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

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

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

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


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

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/shosca/django-sorcery",
    "name": "django-sorcery",
    "maintainer": "",
    "docs_url": null,
    "requires_python": "",
    "maintainer_email": "",
    "keywords": "sqlalchemy django framework forms",
    "author": "Serkan Hosca",
    "author_email": "serkan@hosca.com",
    "download_url": "https://files.pythonhosted.org/packages/45/c2/5285d9b1e9c182b5e853fdd7d1bce66e4a40962983710c01f5694488c6c9/django-sorcery-0.13.0.tar.gz",
    "platform": null,
    "description": "#############################################################\nDjango Sorcery - Django Framework integration with SQLAlchemy\n#############################################################\n\n|Build Status| |Read The Docs| |PyPI version| |Coveralls Status| |Black|\n\n* Free software: MIT license\n* GitHub: https://github.com/shosca/django-sorcery\n\nSQLAlchemy is an excellent orm. And Django is a great framework, until you decide not to use Django ORM. This library\nprovides utilities, helpers and configurations to ease the pain of using SQLAlchemy with Django. It aims to provide\na similar development experience to building a Django application with Django ORM, except with SQLAlchemy.\n\nInstallation\n============\n\n::\n\n    pip install django-sorcery\n\nQuick Start\n===========\n\nLets start by creating a site:\n\n.. code:: console\n\n   $ django-admin startproject mysite\n\nAnd lets create an app:\n\n.. code:: console\n\n   $ cd mysite\n   $ python manage.py startapp polls\n\nThis will create a polls app with standard django app layout:\n\n.. code:: console\n\n   $ tree\n   .\n   \u251c\u2500\u2500 manage.py\n   \u251c\u2500\u2500 polls\n   \u2502\u00a0\u00a0 \u251c\u2500\u2500 admin.py\n   \u2502\u00a0\u00a0 \u251c\u2500\u2500 apps.py\n   \u2502\u00a0\u00a0 \u251c\u2500\u2500 __init__.py\n   \u2502\u00a0\u00a0 \u251c\u2500\u2500 migrations\n   \u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u2514\u2500\u2500 __init__.py\n   \u2502\u00a0\u00a0 \u251c\u2500\u2500 models.py\n   \u2502\u00a0\u00a0 \u251c\u2500\u2500 tests.py\n   \u2502\u00a0\u00a0 \u2514\u2500\u2500 views.py\n   \u2514\u2500\u2500 mysite\n      \u251c\u2500\u2500 __init__.py\n      \u251c\u2500\u2500 settings.py\n      \u251c\u2500\u2500 urls.py\n      \u2514\u2500\u2500 wsgi.py\n\n   3 directories, 12 files\n\nAnd lets add our ``polls`` app and ``django_sorcery`` in ``INSTALLED_APPS`` in ``mysite/settings.py``:\n\n.. code:: python\n\n   INSTALLED_APPS = [\n      'django.contrib.admin',\n      'django.contrib.auth',\n      'django.contrib.contenttypes',\n      'django.contrib.sessions',\n      'django.contrib.messages',\n      'django.contrib.staticfiles',\n      'django_sorcery',\n      'polls.apps.PollsConfig',\n   ]\n\nNow we're going to make a twist and start building our app with ``sqlalchemy``. Lets define our models in\n``polls/models.py``:\n\n.. code:: python\n\n   from django_sorcery.db import databases\n\n\n   db = databases.get(\"default\")\n\n\n   class Question(db.Model):\n      pk = db.Column(db.Integer(), autoincrement=True, primary_key=True)\n      question_text = db.Column(db.String(length=200))\n      pub_date = db.Column(db.DateTime())\n\n\n   class Choice(db.Model):\n      pk = db.Column(db.Integer(), autoincrement=True, primary_key=True)\n      choice_text = db.Column(db.String(length=200))\n      votes = db.Column(db.Integer(), default=0)\n\n      question = db.ManyToOne(Question, backref=db.backref(\"choices\", cascade=\"all, delete-orphan\"))\n\n\nNow that we have some models, lets create a migration using ``alembic`` integration:\n\n.. code:: console\n\n   $ python manage.py sorcery revision -m \"Add question and poll models\" polls\n     Generating ./polls/migrations/3983fc419e10_add_question_and_poll_models.py ... done\n\n\nLet's take a look at the generated migration file ``./polls/migrations/3983fc419e10_add_question_and_poll_models.py``:\n\n.. code:: python\n\n   \"\"\"\n   Add question and poll models\n\n   Revision ID: 3983fc419e10\n   Revises:\n   Create Date: 2019-04-16 20:57:48.154179\n   \"\"\"\n\n   from alembic import op\n   import sqlalchemy as sa\n\n\n   # revision identifiers, used by Alembic.\n   revision = '3983fc419e10'\n   down_revision = None\n   branch_labels = None\n   depends_on = None\n\n\n   def upgrade():\n      # ### commands auto generated by Alembic - please adjust! ###\n      op.create_table('question',\n      sa.Column('pk', sa.Integer(), autoincrement=True, nullable=False),\n      sa.Column('question_text', sa.String(length=200), nullable=True),\n      sa.Column('pub_date', sa.DateTime(), nullable=True),\n      sa.PrimaryKeyConstraint('pk')\n      )\n      op.create_table('choice',\n      sa.Column('pk', sa.Integer(), autoincrement=True, nullable=False),\n      sa.Column('choice_text', sa.String(length=200), nullable=True),\n      sa.Column('votes', sa.Integer(), nullable=True),\n      sa.Column('question_pk', sa.Integer(), nullable=True),\n      sa.ForeignKeyConstraint(['question_pk'], ['question.pk'], ),\n      sa.PrimaryKeyConstraint('pk')\n      )\n      # ### end Alembic commands ###\n\n\n   def downgrade():\n      # ### commands auto generated by Alembic - please adjust! ###\n      op.drop_table('choice')\n      op.drop_table('question')\n      # ### end Alembic commands ###\n\nLet's take a look at generated sql:\n\n.. code:: console\n\n   $ python manage.py sorcery upgrade --sql polls\n\n   CREATE TABLE alembic_version_polls (\n      version_num VARCHAR(32) NOT NULL,\n      CONSTRAINT alembic_version_polls_pkc PRIMARY KEY (version_num)\n   );\n\n   -- Running upgrade  -> d7d86e07cc8e\n\n   CREATE TABLE question (\n      pk INTEGER NOT NULL,\n      question_text VARCHAR(200),\n      pub_date DATETIME,\n      PRIMARY KEY (pk)\n   );\n\n   CREATE TABLE choice (\n      pk INTEGER NOT NULL,\n      choice_text VARCHAR(200),\n      votes INTEGER,\n      question_pk INTEGER,\n      PRIMARY KEY (pk),\n      FOREIGN KEY(question_pk) REFERENCES question (pk)\n   );\n\n   INSERT INTO alembic_version_polls (version_num) VALUES ('d7d86e07cc8e');\n\n\nLet's bring our db up to date:\n\n.. code:: console\n\n   $ python manage.py sorcery upgrade\n   Running migrations for polls on database default\n\n\nRight now, we have enough to hop in django shell:\n\n.. code:: console\n\n   $ python manage.py shell\n\n   >>> from polls.models import Choice, Question, db  # Import the model classes and the db\n\n   # we have no choices or questions in db yet\n   >>> Choice.query.all()\n   []\n   >>> Question.query.all()\n   []\n\n   # Lets create a new question\n   >>> from django.utils import timezone\n   >>> q = Question(question_text=\"What's new?\", pub_date=timezone.now())\n   >>> q\n   Question(pk=None, pub_date=datetime.datetime(2018, 5, 19, 0, 54, 20, 778186, tzinfo=<UTC>), question_text=\"What's new?\")\n\n   # lets save our question, we need to add our question to the db\n   >>> db.add(q)\n\n   # at this point the question is in pending state\n   >>> db.new\n   IdentitySet([Question(pk=None, pub_date=datetime.datetime(2018, 5, 19, 0, 54, 20, 778186, tzinfo=<UTC>), question_text=\"What's new?\")])\n\n   # lets flush to the database\n   >>> db.flush()\n\n   # at this point our question is in persistent state and will receive a primary key\n   >>> q.pk\n   1\n\n   # lets change the question text\n   >>> q.question_text = \"What's up?\"\n   >>> db.flush()\n\n   # Question.objects and Question.query are both query properties that return a query object bound to db\n   >>> Question.objects\n   <django_sorcery.db.query.Query at 0x7feb1c7899e8>\n   >>> Question.query\n   <django_sorcery.db.query.Query at 0x7feb1c9377f0>\n\n   # and lets see all the questions\n   >>> Question.objects.all()\n   [Question(pk=1, pub_date=datetime.datetime(2018, 5, 19, 0, 54, 20, 778186, tzinfo=<UTC>), question_text=\"What's up?\")]\n\n   >>> exit()\n\nLet's add a couple of views in ``polls/views.py``, starting with a list view:\n\n.. code:: python\n\n   from django.shortcuts import render\n   from django.template import loader\n   from django.http import HttpResponseRedirect\n   from django.urls import reverse\n\n   from django_sorcery.shortcuts import get_object_or_404\n\n   from .models import Question, Choice, db\n\n   def index(request):\n      latest_question_list = Question.objects.order_by(Question.pub_date.desc())[:5]\n      context = {'latest_question_list': latest_question_list}\n      return render(request, 'polls/index.html', context)\n\n\n   def detail(request, question_id):\n      question = get_object_or_404(Question, pk=question_id)\n      return render(request, 'polls/detail.html', {'question': question})\n\n\n   def results(request, question_id):\n      question = get_object_or_404(Question, pk=question_id)\n      return render(request, 'polls/results.html', {'question': question})\n\n\n   def vote(request, question_id):\n      question = get_object_or_404(Question, pk=question_id)\n\n      selected_choice = Choice.query.filter(\n         Choice.question == question,\n         Choice.pk == request.POST['choice'],\n      ).one_or_none()\n\n      if not selected_choice:\n         return render(request, 'polls/detail.html', {\n               'question': question,\n               'error_message': \"You didn't select a choice.\",\n         })\n\n      selected_choice.votes += 1\n      db.flush()\n      return HttpResponseRedirect(reverse('polls:results', args=(question.pk,)))\n\nand register the view in ``polls/urls.py``:\n\n.. code:: python\n\n   from django.urls import path\n\n   from . import views\n\n\n   app_name = 'polls'\n   urlpatterns = [\n      path('', views.index, name='index'),\n      path('<int:question_id>/', views.detail, name='detail'),\n      path('<int:question_id>/results', views.results, name='results'),\n      path('<int:question_id>/vote', views.vote, name='vote'),\n   ]\n\nand register the ``SQLAlchemyMiddleware`` to provide unit-of-work per request pattern:\n\n.. code:: python\n\n   MIDDLEWARE = [\n      'django_sorcery.db.middleware.SQLAlchemyMiddleware',\n      # ...\n   ]\n\nand add some templates:\n\n``polls/templates/polls/index.html``:\n\n.. code:: html\n\n   {% if latest_question_list %}\n   <ul>\n   {% for question in latest_question_list %}\n   <li><a href=\"{% url 'polls:detail' question.pk %}\">{{ question.question_text }}</a></li>\n   {% endfor %}\n   </ul>\n   {% else %}\n   <p>No polls are available.</p>\n   {% endif %}\n\n``polls/templates/polls/detail.html``:\n\n.. code:: html\n\n   <h1>{{ question.question_text }}</h1>\n\n   {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}\n\n   <form action=\"{% url 'polls:vote' question.pk %}\" method=\"post\">\n   {% csrf_token %}\n   {% for choice in question.choices %}\n      <input type=\"radio\" name=\"choice\" id=\"choice{{ forloop.counter }}\" value=\"{{ choice.pk }}\" />\n      <label for=\"choice{{ forloop.counter }}\">{{ choice.choice_text }}</label><br />\n   {% endfor %}\n   <input type=\"submit\" value=\"Vote\" />\n   </form>\n\n\n``polls/templates/polls/results.html``:\n\n.. code:: html\n\n   <h1>{{ question.question_text }}</h1>\n\n   <ul>\n   {% for choice in question.choices %}\n      <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>\n   {% endfor %}\n   </ul>\n\n   <a href=\"{% url 'polls:detail' question.pk %}\">Vote again?</a>\n\nThis is all fine but we can do one better using generic views. Lets adjust our views in ``polls/views.py``:\n\n.. code:: python\n\n   from django.shortcuts import render\n   from django.http import HttpResponseRedirect\n   from django.urls import reverse\n\n   from django_sorcery.shortcuts import get_object_or_404\n   from django_sorcery import views\n\n   from .models import Question, Choice, db\n\n\n   class IndexView(views.ListView):\n      template_name = 'polls/index.html'\n      context_object_name = 'latest_question_list'\n\n      def get_queryset(self):\n         return Question.objects.order_by(Question.pub_date.desc())[:5]\n\n\n   class DetailView(views.DetailView):\n      model = Question\n      session = db\n      template_name = 'polls/detail.html'\n\n\n   class ResultsView(DetailView):\n      template_name = 'polls/results.html'\n\n\n   def vote(request, question_id):\n      question = get_object_or_404(Question, pk=question_id)\n\n      selected_choice = Choice.query.filter(\n         Choice.question == question,\n         Choice.pk == request.POST['choice'],\n      ).one_or_none()\n\n      if not selected_choice:\n         return render(request, 'polls/detail.html', {\n               'question': question,\n               'error_message': \"You didn't select a choice.\",\n         })\n\n      selected_choice.votes += 1\n      db.flush()\n      return HttpResponseRedirect(reverse('polls:results', args=(question.pk,)))\n\nand adjust the ``polls/urls.py`` like:\n\n.. code:: python\n\n   from django.urls import path\n\n   from . import views\n\n\n   app_name = 'polls'\n   urlpatterns = [\n      path('', views.IndexView.as_view(), name='index'),\n      path('<int:pk>/', views.DetailView.as_view(), name='detail'),\n      path('<int:pk>/results', views.ResultsView.as_view(), name='results'),\n      path('<int:question_id>/vote', views.vote, name='vote'),\n   ]\n\nThe default values for ``template_name`` and ``context_object_name`` are similar to django's generic views. If we\nhandn't defined those the default for template names would've been ``polls/question_detail.html`` and\n``polls/question_list.html`` for the detail and list template names, and ``question`` and ``question_list`` for context\nnames for detail and list views.\n\nThis is all fine but we can even do one better using a viewset. Lets adjust our views in ``polls/views.py``:\n\n.. code:: python\n\n   from django.http import HttpResponseRedirect\n   from django.urls import reverse, reverse_lazy\n\n   from django_sorcery.routers import action\n   from django_sorcery.viewsets import ModelViewSet\n\n   from .models import Question, Choice, db\n\n\n   class PollsViewSet(ModelViewSet):\n      model = Question\n      fields = \"__all__\"\n      destroy_success_url = reverse_lazy(\"polls:question-list\")\n\n      def get_success_url(self):\n         return reverse(\"polls:question-detail\", kwargs={\"pk\": self.object.pk})\n\n      @action(detail=True)\n      def results(self, request, *args, **kwargs):\n         return self.retrieve(request, *args, **kwargs)\n\n      @action(detail=True, methods=[\"POST\"])\n      def vote(self, request, *args, **kwargs):\n         self.object = self.get_object()\n\n         selected_choice = Choice.query.filter(\n               Choice.question == self.object, Choice.pk == request.POST.get(\"choice\")\n         ).one_or_none()\n\n         if not selected_choice:\n               context = self.get_detail_context_data(object=self.object)\n               context[\"error_message\"] = \"You didn't select a choice.\"\n               self.action = \"retrieve\"\n               return self.render_to_response(context)\n\n         selected_choice.votes += 1\n         db.flush()\n         return HttpResponseRedirect(reverse(\"polls:question-results\", args=(self.object.pk,)))\n\nAnd adjusting our ``polls/urls.py`` like:\n\n.. code:: python\n\n   from django.urls import path, include\n\n   from django_sorcery.routers import SimpleRouter\n\n   from . import views\n\n   router = SimpleRouter()\n   router.register(\"\", views.PollsViewSet)\n\n   app_name = \"polls\"\n   urlpatterns = [path(\"\", include(router.urls))]\n\nWith these changes we'll have the following urls:\n\n.. code:: console\n\n   $ ./manage.py run show_urls\n   /polls/\tpolls.views.PollsViewSet\tpolls:question-list\n   /polls/<pk>/\tpolls.views.PollsViewSet\tpolls:question-detail\n   /polls/<pk>/delete/\tpolls.views.PollsViewSet\tpolls:question-destroy\n   /polls/<pk>/edit/\tpolls.views.PollsViewSet\tpolls:question-edit\n   /polls/<pk>/results/\tpolls.views.PollsViewSet\tpolls:question-results\n   /polls/<pk>/vote/\tpolls.views.PollsViewSet\tpolls:question-vote\n   /polls/new/\tpolls.views.PollsViewSet\tpolls:question-new\n\nThis will map the following operations to following actions on the viewset:\n\n====== ======================== =============== ===============\nMethod Path                     Action          Route Name\n====== ======================== =============== ===============\nGET    /polls/                  list            question-list\nPOST   /polls/                  create          question-list\nGET    /polls/new/              new             question-new\nGET    /polls/1/                retrieve        question-detail\nPOST   /polls/1/                update          question-detail\nPUT    /polls/1/                update          question-detail\nPATCH  /polls/1/                update          question-detail\nDELETE /polls/1/                destroy         question-detail\nGET    /polls/1/edit/           edit            question-edit\nGET    /polls/1/delete/         confirm_destoy  question-delete\nPOST   /polls/1/delete/         destroy         question-delete\n====== ======================== =============== ===============\n\nNow, lets add an inline formset to be able to add choices to questions, adjust ``polls/views.py``:\n\n.. code:: python\n\n   from django.http import HttpResponseRedirect\n   from django.urls import reverse, reverse_lazy\n\n   from django_sorcery.routers import action\n   from django_sorcery.viewsets import ModelViewSet\n   from django_sorcery.formsets import inlineformset_factory\n\n   from .models import Question, Choice, db\n\n\n   ChoiceFormSet = inlineformset_factory(relation=Question.choices, fields=(Choice.choice_text.key,), session=db)\n\n\n   class PollsViewSet(ModelViewSet):\n      model = Question\n      fields = (Question.question_text.key, Question.pub_date.key)\n      destroy_success_url = reverse_lazy(\"polls:question-list\")\n\n      def get_success_url(self):\n         return reverse(\"polls:question-detail\", kwargs={\"pk\": self.object.pk})\n\n      def get_form_context_data(self, **kwargs):\n         kwargs[\"choice_formset\"] = self.get_choice_formset()\n         return super().get_form_context_data(**kwargs)\n\n      def get_choice_formset(self, instance=None):\n         if not hasattr(self, \"_choice_formset\"):\n               instance = instance or self.object\n               self._choice_formset = ChoiceFormSet(\n                  instance=instance, data=self.request.POST if self.request.POST else None\n               )\n\n         return self._choice_formset\n\n      def process_form(self, form):\n         if form.is_valid() and self.get_choice_formset(instance=form.instance).is_valid():\n               return self.form_valid(form)\n\n         return form.invalid(self, form)\n\n      def form_valid(self, form):\n         self.object = form.save()\n         self.object.choices = self.get_choice_formset().save()\n         db.flush()\n         return HttpResponseRedirect(self.get_success_url())\n\n      @action(detail=True)\n      def results(self, request, *args, **kwargs):\n         return self.retrieve(request, *args, **kwargs)\n\n      @action(detail=True, methods=[\"POST\"])\n      def vote(self, request, *args, **kwargs):\n         self.object = self.get_object()\n\n         selected_choice = Choice.query.filter(\n               Choice.question == self.object, Choice.pk == request.POST.get(\"choice\")\n         ).one_or_none()\n\n         if not selected_choice:\n               context = self.get_detail_context_data(object=self.object)\n               context[\"error_message\"] = \"You didn't select a choice.\"\n               self.action = \"retrieve\"\n               return self.render_to_response(context)\n\n         selected_choice.votes += 1\n         db.flush()\n         return HttpResponseRedirect(reverse(\"polls:question-results\", args=(self.object.pk,)))\n\nAnd add ``choice_formset`` in the ``polls/templates/question_edit.html`` and ``polls/templates/question_edit.html``\n\n.. code:: html\n\n   <form ... >\n      ...\n      {{ choice_formset }}\n      ...\n   </form >\n\n\n.. |Build Status| image:: https://github.com/shosca/django-sorcery/workflows/Build/badge.svg?branch=master\n   :target: https://github.com/shosca/django-sorcery/actions?query=workflow%3ABuild+branch%3Amaster\n.. |Read The Docs| image:: https://readthedocs.org/projects/django-sorcery/badge/?version=latest\n   :target: http://django-sorcery.readthedocs.io/en/latest/?badge=latest\n.. |PyPI version| image:: https://badge.fury.io/py/django-sorcery.svg\n   :target: https://badge.fury.io/py/django-sorcery\n.. |Coveralls Status| image:: https://coveralls.io/repos/github/shosca/django-sorcery/badge.svg?branch=master\n   :target: https://coveralls.io/github/shosca/django-sorcery?branch=master\n.. |Black| image:: https://img.shields.io/badge/code%20style-black-000000.svg\n   :target: https://github.com/ambv/black\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Django and SQLAlchemy integration",
    "version": "0.13.0",
    "split_keywords": [
        "sqlalchemy",
        "django",
        "framework",
        "forms"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "342ab767b0d97245a65c65d2a7796fecb8659b8439bc69a29c6843aa95597584",
                "md5": "f7543f9d7cc4670c5472d58d4e13af79",
                "sha256": "841e112f3b3a0d85cbcd6fc2a8f0e5bae4cc4c433310cc85919001a436e4dd62"
            },
            "downloads": -1,
            "filename": "django_sorcery-0.13.0-py2.py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "f7543f9d7cc4670c5472d58d4e13af79",
            "packagetype": "bdist_wheel",
            "python_version": "py2.py3",
            "requires_python": null,
            "size": 92109,
            "upload_time": "2023-04-11T11:20:13",
            "upload_time_iso_8601": "2023-04-11T11:20:13.414950Z",
            "url": "https://files.pythonhosted.org/packages/34/2a/b767b0d97245a65c65d2a7796fecb8659b8439bc69a29c6843aa95597584/django_sorcery-0.13.0-py2.py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "45c25285d9b1e9c182b5e853fdd7d1bce66e4a40962983710c01f5694488c6c9",
                "md5": "4b4829736a38ad198dc4b5edb5e9bc85",
                "sha256": "d7df9d110ebcb3731c90ad813f9006f12f19c45e75f0fbb54cebe6afc3feddc2"
            },
            "downloads": -1,
            "filename": "django-sorcery-0.13.0.tar.gz",
            "has_sig": false,
            "md5_digest": "4b4829736a38ad198dc4b5edb5e9bc85",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 132869,
            "upload_time": "2023-04-11T11:20:16",
            "upload_time_iso_8601": "2023-04-11T11:20:16.237292Z",
            "url": "https://files.pythonhosted.org/packages/45/c2/5285d9b1e9c182b5e853fdd7d1bce66e4a40962983710c01f5694488c6c9/django-sorcery-0.13.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-04-11 11:20:16",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "github_user": "shosca",
    "github_project": "django-sorcery",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "requirements": [],
    "tox": true,
    "lcname": "django-sorcery"
}
        
Elapsed time: 0.05462s