popolo - code simple..

popolo - code simple..

Masonite Project - Rich-text editor with TinyMCE

Masonite Project - Rich-text editor with TinyMCE

Theodoros Kafantaris's photo
Theodoros Kafantaris
·May 8, 2022·

8 min read

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:

  1. index
  2. create
  3. store
  4. show
  5. edit
  6. update
  7. 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.

filesystem.py

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.

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

image.png

create page

image.png

edit page

image.png

show page

image.png

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:

Github Repo

 
Share this