In this tutorial we will see how to create a Masonite CRUD web application with rich-text editor with TinyMCE. We will follow a few steps and we will get basic crud with TinyMCE feature using controller, model, route, bootstrap 5, and html templates.
Masonite Installation
mkdir crud-with-tinymce
cd crud-with-tinymce
Activating Our Virtual Environment
python -m venv venv
source venv/bin/activate
Install Masonite
pip install masonite
Start project
project start .
Start Development Server
python craft serve
This is it! We have the new Masonite Project running now!
Database Configuration
In this step, we will configure mysql DB in our .env
. We need to add database name, mysql username and password.
So we need to create in our mysql server a DB with name crud-with-tinymce
.
DB_CONNECTION=mysql
#SQLITE_DB_DATABASE=masonite.sqlite3
DB_HOST=127.0.0.1
DB_USERNAME=root
DB_PASSWORD=
DB_DATABASE=crud-with-tinymce
DB_PORT=3306
DB_LOG=True
Create Migration
Here, we will create products
table using masonite craft migration. So let's use following command to create migration file.
python craft migration create_products_table --create products
After this command you will find one file in the following path database/migrations
and you have to put code below in your migration file for creating the products table.
"""CreateProductsTable Migration."""
from masoniteorm.migrations import Migration
class CreateProductsTable(Migration):
def up(self):
"""
Run the migrations.
"""
with self.schema.create("products") as table:
table.increments("id")
table.string("name")
table.text("details")
table.string("image")
table.timestamps()
def down(self):
"""
Revert the migrations.
"""
self.schema.drop("products")
Now we will run this migration by the following command:
python craft migrate
Create Controller and Model
In this step, now we should create new controller as ProductController. So run bellow command and create new controller.
python craft controller ProductController
After the bellow command, you will find a new file in this path app/controllers/ProductController.py
.
In this controller we will create seven methods as bellow methods:
- index
- create
- store
- show
- edit
- update
- delete
app/controllers/ProductController.py
from email.mime import image
from masonite.request import Request
from masonite.controllers import Controller
from masonite.views import View
from masonite.response import Response
from app.models.Product import Product
from masonite.validation import Validator
from masonite.filesystem import Storage
from masonite.facades import Dump
class ProductController(Controller):
def index(self, view: View):
products = Product.all()
return view.render('products/index', {'products': products})
def create(self, view: View):
return view.render('products/create')
def store(self, storage: Storage, request: Request, response: Response, validate: Validator):
# Dump.dump(request.input('image'))
errors = request.validate(
validate.required(['name', 'details']),
# validate.file('image', mimes=['pdf', 'txt'])
)
if errors:
return response.redirect('/products/create').with_errors(errors)
path = storage.disk('local').put_file('public', request.input('image'))
# Dump.dd(str(path))
Product.create(
name=request.input('name'),
details=request.input('details'),
image = path
)
return response.redirect('/products')
def show(self, view: View, request: Request):
product = Product.where('id', request.param('product_id')).first()
return view.render('products/show', {'product': product})
def edit(self, view: View, request: Request):
product = Product.where('id', request.param('product_id')).first()
return view.render('products/edit', {'product': product})
def update(self, request: Request, storage: Storage ,response: Response, validate: Validator):
errors = request.validate(
validate.required(['name', 'details']),
)
if errors:
return response.redirect('/products/edit/{}'.format(request.input('id'))).with_errors(errors)
product = Product.where('id', request.input('id')).first()
if request.input('image').name:
path = storage.disk('local').put_file('public', request.input('image'))
product.image = path
product.name = request.input('name')
product.details = request.input('details')
product.save()
return response.redirect('/products')
def delete(self, request: Request, response: Response):
product = Product.where('id', request.param('product_id')).first()
product.delete()
return response.redirect('/products')
We need to update the congif/filesystem.py
file accordingly in order to prepare the path for file uploads.
We are going to use the local disk and especially storage
folder to upload our files.
from masonite.environment import env
from masonite.utils.location import base_path
DISKS = {
"default": "local",
"local": {"driver": "file", "path": base_path("storage")},
"s3": {
"driver": "s3",
"client": env("S3_CLIENT"),
"secret": env("S3_SECRET"),
"bucket": env("S3_BUCKET"),
},
}
STATICFILES = {
# folder # template alias
"storage/static": "static/",
"storage/compiled": "assets/",
"storage/public": "/",
}
In order to have shared individual input validation errors we need to import and register the following Middleware (ShareErrorsInSessionMiddleware
) in our Kernel.py
.
from masonite.foundation import response_handler
from masonite.storage import StorageCapsule
from masonite.auth import Sign
from masonite.environment import LoadEnvironment
from masonite.utils.structures import load
from masonite.utils.location import base_path
from masonite.middleware import (
SessionMiddleware,
EncryptCookies,
ShareErrorsInSessionMiddleware,
LoadUserMiddleware,
MaintenanceModeMiddleware,
)
from masonite.routes import Route
from masonite.configuration.Configuration import Configuration
from masonite.configuration import config
from app.middlewares import VerifyCsrfToken, AuthenticationMiddleware
class Kernel:
http_middleware = [MaintenanceModeMiddleware, EncryptCookies]
route_middleware = {
"web": [SessionMiddleware, LoadUserMiddleware, VerifyCsrfToken, ShareErrorsInSessionMiddleware],
"auth": [AuthenticationMiddleware],
}
def __init__(self, app):
self.application = app
def register(self):
# Register routes
self.load_environment()
self.register_configurations()
self.register_middleware()
self.register_routes()
self.register_database()
self.register_templates()
self.register_storage()
def load_environment(self):
LoadEnvironment()
def register_configurations(self):
# load configuration
self.application.bind("config.location", "config")
configuration = Configuration(self.application)
configuration.load()
self.application.bind("config", configuration)
key = config("application.key")
self.application.bind("key", key)
self.application.bind("sign", Sign(key))
# set locations
self.application.bind("resources.location", "resources/")
self.application.bind("controllers.location", "app/controllers")
self.application.bind("jobs.location", "app/jobs")
self.application.bind("providers.location", "app/providers")
self.application.bind("mailables.location", "app/mailables")
self.application.bind("listeners.location", "app/listeners")
self.application.bind("validation.location", "app/validation")
self.application.bind("notifications.location", "app/notifications")
self.application.bind("events.location", "app/events")
self.application.bind("tasks.location", "app/tasks")
self.application.bind("models.location", "app/models")
self.application.bind("observers.location", "app/models/observers")
self.application.bind("policies.location", "app/policies")
self.application.bind("commands.location", "app/commands")
self.application.bind("middlewares.location", "app/middlewares")
self.application.bind("server.runner", "masonite.commands.ServeCommand.main")
def register_middleware(self):
self.application.make("middleware").add(self.route_middleware).add(self.http_middleware)
def register_routes(self):
Route.set_controller_locations(self.application.make("controllers.location"))
self.application.bind("routes.location", "routes/web")
self.application.make("router").add(
Route.group(
load(self.application.make("routes.location"), "ROUTES"), middleware=["web"]
)
)
def register_database(self):
from masoniteorm.query import QueryBuilder
self.application.bind(
"builder",
QueryBuilder(connection_details=config("database.databases")),
)
self.application.bind("migrations.location", "databases/migrations")
self.application.bind("seeds.location", "databases/seeds")
self.application.bind("resolver", config("database.db"))
def register_templates(self):
self.application.bind("views.location", "templates/")
def register_storage(self):
storage = StorageCapsule()
storage.add_storage_assets(config("filesystem.staticfiles"))
self.application.bind("storage_capsule", storage)
self.application.set_response_handler(response_handler)
self.application.use_storage_path(base_path("storage"))
It is now the time to create our Product
model.
python craft model Product
app/models/Product.py
""" Product Model """
from masoniteorm.models import Model
class Product(Model):
__fillable__ = ['name', 'details', 'image']
Add Routes
We need to add routes for product crud application. So we open our "routes/web.py" file and add following route.
from masonite.routes import Route
ROUTES = [Route.get("/", "ProductController@index"),
Route.get("/products", "ProductController@index"),
Route.get("/products/create", "ProductController@create"),
Route.post("/products", "ProductController@store"),
Route.get("/products/show/@product_id", "ProductController@show"),
Route.get("/products/edit/@product_id", "ProductController@edit"),
Route.post("/products/update", "ProductController@update"),
Route.get('/products/delete/@product_id', 'ProductController@delete'),
]
Add template files
In this step we have to create template files. So mainly we have to create base file and then create new folder "products" then create html files of crud app.
templates/base.html
<!DOCTYPE html>
<html>
<head>
<title>Masonite 4 CRUD Application - blog.popolo.dev</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
{% block head %} {% endblock %}
</head>
<body>
<div class="container">{% block content %} {% endblock %}</div>
{% block script %} {% endblock %}
</body>
</html>
templates/products/index.html
{% extends "base.html" %} {% block content %}
<div class="row">
<div class="col-lg-12 d-flex justify-content-between mt-4">
<div class="pull-left">
<h2>Masonite 4 CRUD Example from scratch - blog.popolo.dev</h2>
</div>
<div class="pull-right mb-4">
<a class="btn btn-success" href="products/create"> Create New Product</a>
</div>
</div>
</div>
<table class="table table-bordered">
<tr>
<th>No</th>
<th>Name</th>
<th>Details</th>
<th>Photo</th>
<th width="280px">Action</th>
</tr>
@for product in products
<tr>
<td>{{product.id}}</td>
<td>{{product.name}}</td>
<td>{{product.details | safe}}</td>
<td>
<img src="/{{ product.image }}" width="100px" height="100px">
</td>
<td>
<form action="" method="POST">
<a class="btn btn-info" href="/products/show/{{product.id}}">Show</a>
<a class="btn btn-primary" href="/products/edit/{{product.id}}">Edit</a>
<a class="btn btn-danger" href="/products/delete/{{product.id}}">Delete</a>
</form>
</td>
</tr>
@endfor
</table>
{% endblock %}
templates/products/create.html
{% extends "base.html" %}
{% block head %}
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-12 mt-4 d-flex justify-content-between">
<div class="pull-left">
<h2>Add New Product</h2>
</div>
<div class="pull-right">
<a class="btn btn-primary" href="/products"> Back</a>
</div>
</div>
</div>
<form action="/products" method="POST" enctype="multipart/form-data">
{{ csrf_field }}
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12">
<div class="form-group">
<strong>Name:</strong>
<input type="text" name="name" class="form-control" placeholder="Name">
</div>
@if errors.has('name')
<div class="text-danger">
<code>{{ errors.get('name')[0] }}</code>
</div>
@endif
</div>
<div class=" col-xs-12 col-sm-12 col-md-12">
<div class="form-group">
<strong>Detail:</strong>
<textarea class="tinymce-editor" style="height:150px" name="details" ></textarea>
</div>
@if errors.has('details')
<div class="text-danger">
<code>{{ errors.get('details')[0] }}</code>
</div>
@endif
</div>
<div class="mb-2 col-xs-12 col-sm-12 col-md-12">
<div class="form-group">
<strong>Image:</strong>
<input type="file" name="image" class="form-control" placeholder="image" accept="image/*">
</div>
@if errors.has('image')
<div class="text-danger">
<code>{{ errors.get('image')[0] }}</code>
</div>
@endif
</div>
<div class="col-xs-12 col-sm-12 col-md-12 text-center">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</div>
</form>
{% endblock %}
{% block script %}
<script type="text/javascript">
tinymce.init({
selector: 'textarea.tinymce-editor',
placeholder: 'Type here...',
height: 300,
menubar: false,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount', 'image'
],
toolbar: 'undo redo | formatselect | ' +
'bold italic backcolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | help',
content_css: '//www.tiny.cloud/css/codepen.min.css'
});
</script>
{% endblock %}
templates/products/edit.html
{% extends "base.html" %}
{% block head %}
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-12 mt-4 d-flex justify-content-between">
<div class="pull-left">
<h2>Edit Product - {{product.name}}</h2>
</div>
<div class="pull-right">
<a class="btn btn-primary" href="/products"> Back</a>
</div>
</div>
</div>
<form action="/products/update" method="POST" enctype="multipart/form-data">
{{ csrf_field }}
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12">
<input type="hidden" name="id" value="{{product.id}}" >
<div class="form-group">
<strong>Name:</strong>
<input type="text" name="name" value="{{product.name}}" class="form-control" placeholder="Name">
</div>
@if errors.has('name')
<div class="text-danger">
<code>{{ errors.get('name')[0] }}</code>
</div>
@endif
</div>
<div class=" col-xs-12 col-sm-12 col-md-12">
<div class="form-group">
<strong>Detail:</strong>
<textarea class="tinymce-editor" style="height:150px" name="details" >{{product.details | safe}}</textarea>
</div>
@if errors.has('details')
<div class="text-danger">
<code>{{ errors.get('details')[0] }}</code>
</div>
@endif
</div>
<div class="mb-2 col-xs-12 col-sm-12 col-md-12">
<div class="form-group">
<strong>Image:</strong>
<input type="file" name="image" class="form-control">
</div>
@if errors.has('image')
<div class="text-danger">
<code>{{ errors.get('image')[0] }}</code>
</div>
@endif
</div>
<div class="col-xs-12 col-sm-12 col-md-12 text-center">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</div>
</form>
{% endblock %}
{% block script %}
<script type="text/javascript">
tinymce.init({
selector: 'textarea.tinymce-editor',
placeholder: 'Type here...',
height: 300,
menubar: false,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount', 'image'
],
toolbar: 'undo redo | formatselect | ' +
'bold italic backcolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | help',
content_css: '//www.tiny.cloud/css/codepen.min.css'
});
</script>
{% endblock %}
templates/products/show.html
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="mt-4 col-lg-12 mb-2 d-flex justify-content-between">
<div class="pull-left">
<h2> Show Product</h2>
</div>
<div class="pull-right">
<a class="btn btn-primary" href="/products"> Back</a>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12">
<div class="form-group">
<strong>Name:</strong>
{{ product.name }}
</div>
</div>
<div class="col-xs-12 col-sm-12 col-md-12">
<div class="form-group">
<strong>Details:</strong>
{{ product.details }}
</div>
</div>
<div class="col-xs-12 col-sm-12 col-md-12">
<div class="form-group">
<strong>Image:</strong>
<img src="/{{ product.image }}" width="100px" height="100px">
</div>
</div>
</div>
{% endblock %}
Visit application
Now, open your web browser, type the given URL and view the app output:
http://127.0.0.1:8000/products
index page
create page
edit page
show page
That's it! Now we have our CRUD application with rich text editor TineMCE!
You can download the code for the project from the repo below: