CRUD for your model¶
Accelerate prototyping with basic CRUD (Create, Read, Update, Delete) python views and HTML templates, enhanced with htmx and Tailwind CSS.
CRUD¶
Usage: crud [--blueprints BLUEPRINTS] [-e EXCLUDED_FIELDS] [--only-python] [--only-html] [--entry-point] [-l] [--skip-git-check] MODEL_PATH [-h] [--completion COMPLETION]
Generate CRUD (Create, Read, Update, Delete) views for a model.
Options
[--blueprints BLUEPRINTS] The path to custom html templates that will serve as blueprints. (Default: )
[-e, --exclude EXCLUDED_FIELDS] Fields to exclude from the views, forms and templates. (Default: [])
[--only-python] Generate only python code. (Default: False)
[--only-html] Generate only html code. (Default: False)
[--entry-point] Use the specified model as the entry point of the app. (Default: False)
[-l, --login-required] Add the login_required decorator to all views. (Default: False)
[--skip-git-check] Do not check if your git repo is clean. (Default: False)
Arguments
MODEL_PATH The path (<app_label>.<model_name>) of the model to generate CRUD views for. Ex: myapp.product
Help
[-h, --help] Show this message and exit.
[--completion COMPLETION] Use --completion generate to print shell-specific completion source. Valid options: generate, complete.
Warning
Make sure you have installed falco-toolbox before running this command. If you are using a falco-generated project, it will already be installed.
This command generates htmx-powered create, read, update, and delete views for your model. It follows a similar idea
as neapolitan, but with a completely different approach. Instead of inheriting
from a class as you would with neapolitan, this command generates basic views, urls, forms, admin (thanks to django-extensions)
and HTML templates, and updates or overrides the corresponding files in your project. I prefer this approach because, at the end, you’ll have all the new code directly in front of you. It’s easily
accessible and you can update it as you see fit. The idea is to accelerate project prototyping. Write a model and you instantly have views ready for it.
Why function based views?
I think class-based views get complex faster than function-based views. Both have their use cases, but function-based views stay simpler to manage longer in my experience. There is an excellent document on the topic, read django views the right way.
Configuration¶
There are some options that you may want to set each time you generate CRUD views for a model. For instance, most of your views might require user
login, or you might have a specific set of HTML templates that you use every time you run the command. Typing the same options repeatedly can be tedious.
For such scenarios, some of the CLI options can be configured via the pyproject.toml file.
Here is an example illustrating all available configurations:
[tool.falco.crud]
utils-path = "apps_dir/core"
blueprints = "blueprints"
login-required = true
skip-git-check = true
always-migrate = true
Note
All options are optional.
Keys description
utils-path: This will be written by the install-crud-utils command. Unless you are changing where the utils are installed, you don’t need to worry about this.
blueprints: If you are using custom blueprints for your html, set the path here. It works exactly the same as the equivalent CLI option.
login-required: Always generate views that are decorated with the login_required decorator.
skip-git-check: (Not recommended) This option is for those who like to live dangerously. It will always skip the git check.
always-migrate: This option can only be set in the pyproject.toml file. My current workflow is to create a new app, add fields to a model and then run crud.
I often forget to makemigrations and migrate. This can cause the admin generation code to fail. With this option set, the crud command will first try to
run makemigrations and migrate. If either of these operations fails, the command will stop and print the error.
Python code¶
All Python code added by this command will be in append mode, meaning it won’t override the content of your existing files.
Instead, it will add code at the end or create the files if they are missing. The files that will be modified
are forms.py, urls.py, admin.py (if you have django-extension installed),
views.py and your project root urls.py.
For the sake brevity, I’ll only show an example of what the urls.py file might look like for a model named Entry in a django app named entries.
falco crud entry.entries
from django.urls import path
from . import views
app_name = "entries"
urlpatterns = [
path("entries/", views.entry_list, name="entry_list"),
path("entries/create/", views.entry_create, name="entry_create"),
path("entries/<int:pk>/", views.entry_detail, name="entry_detail"),
path("entries/<int:pk>/update/", views.entry_update, name="entry_update"),
path("entries/<int:pk>/delete/", views.entry_delete, name="entry_delete"),
]
As you can see, the convention is quite simple: <model_name_lower>_<operation>. Note that if you don’t specify the model name and run
falco crud entries, the same code with the described conventions will be generated for all the models in the entries app.
Now, if you’re anything like me, the code above might have made you cringe due to the excessive repetitions of the word entry.
This wouldn’t have been the case if the model was called Category, for example. For these specific cases, there is an --entry-point option.
Let’s try it.
falco crud entries.entry --entry-point
Oops, I made a mistake
If you made a mistake when running CRUD commands and want to discard the changes and restart, you can use the following commands instead of manually deleting all changes. Note that the commands below will discard all current changes. You can also specify a specific path to remove only a subset of the changes.
git checkout -- . # Remove changes in the working tree
git clean -fd # Remove untracked files and directories from the working tree
from django.urls import path
from . import views
app_name = "entries"
urlpatterns = [
path("", views.index, name="index"),
path("create/", views.create, name="create"),
path("<int:pk>/", views.detail, name="detail"),
path("<int:pk>/update/", views.update, name="update"),
path("<int:pk>/delete/", views.delete, name="delete"),
]
Much cleaner, specifying that option means you consider the Entry model as the entry point of your entries app.
So, instead of the base URL of the app looking like entries/entries/, it will just be entries/.
As previously mentioned, the command will also register your app in your project root URLs configuration. This occurs when
you generate crud views for a model and there is no existing urls.py file for the app. In such cases, it is assumed
that you haven’t already registered the URLs for your app since the command just created the file.
Here is an example of how the entries app will be registered.
1urlpatterns = [
2path("admin/", admin.site.urls),
3...
4path("entries/", include("entries.urls", namespace="entries"))
5]
HTML templates¶
Unlike the Python code, the generated HTML templates will overwrite any existing ones. If you want to avoid this, you should commit
your changes before running this command or use the --only-python option to generate only Python code. The files are generated
with minimal styling (using Tailwind CSS) and are reasonably presentable.
Four files are generated:
<model_name_lower>_list.html<model_name_lower>_create.html<model_name_lower>_detail.html<model_name_lower>_update.html
There is no <model_name_lower>_delete.html file because deletion is handled in the <model_name_lower>_list.html.
Each generated HTML file extends a base.html template. Therefore, make sure you have a top-level base.html file in
your templates directory.
Note
If you use the --entry-point option, the files will be named index.html, create.html, detail.html, and update.html.
To determine where to place the generated files, we check the DIRS key in the TEMPLATES settings of your Django project.
If it is populated, we take the first value in the list and generate the template files in <templates_dir>/<app_label>.
If it is not populated, we use the classic Django layout, which is <app_label>/templates/<app_label>.
Custom Templates¶
The crud command supports the ability to specify your own HTML templates using the --blueprints option.
This option only takes into account HTML files and will completely override the default templates. The HTML templates
use the jinja2 syntax. To see examples of what the templates look like,
check out the base templates here.
Below is an example of the context each template will receive.
from falco_cli.commands.crud.model_crud import HtmlBlueprintContext
from falco_cli.commands.crud.model_crud import get_html_blueprint_context
from falco_cli.commands.crud.model_crud import DjangoModel
from pprint import pprint
dj_model = DjangoModel(
name = "Entry",
name_plural = "Entries",
verbose_name = "Entry",
verbose_name_plural = "Entries",
has_file_field = False,
has_editable_date_field = False,
fields = {
"name": {"verbose_name": "Name", "editable": True, "class_name": "CharField", "accessor": "{{entry.name}}"},
"price": {"verbose_name": "Price", "editable": True, "class_name": "DecimalField", "accessor": "{{entry.price}}"},
}
)
pprint(get_html_blueprint_context(app_label="entries", django_model=dj_model), sort_dicts=False, compact=True, width=120)
{'app_label': 'entries',
'model_name': 'Entry',
'model_name_plural': 'Entries',
'model_verbose_name': 'Entry',
'model_verbose_name_plural': 'Entries',
'model_has_file_fields': False,
'model_fields': {'name': {'verbose_name': 'Name',
'editable': True,
'class_name': 'CharField',
'accessor': '{{entry.name}}'},
'price': {'verbose_name': 'Price',
'editable': True,
'class_name': 'DecimalField',
'accessor': '{{entry.price}}'}},
'list_view_url': "{% url 'entries:entry_list' %}",
'create_view_url': "{% url 'entries:entry_create' %}",
'detail_view_url': "{% url 'entries:entry_detail' entry.pk %}",
'update_view_url': "{% url 'entries:entry_update' entry.pk %}",
'delete_view_url': "{% url 'entries:entry_delete' entry.pk %}"}
Examples¶
Some usage examples.
$ falco crud entries.entry
$ falco crud entries
$ falco crud entries.entry -e="secret_field1" -e="secret_field2"
$ falco crud entries.entry --only-html
$ falco crud entries.entry --only-python
$ falco crud entries.entry --entry-point
$ falco crud entries.entry --entry-point --login
$ falco crud entries.entry --blueprints /path/to/blueprints