Setfield.jl: Update Deeply Nested Immutable Structs

Setfield

Update deeply nested immutable structs.

Lifecycle

We plan to maintain Setfield.jl for a long time (written 2020-09-21, reinforced 2021-08-01, 2022-09-08). We will however not add new features. For a successor, see Accessors.jl.

Usage

Updating deeply nested immutable structs was never easier:

using Setfield
@set obj.a.b.c = d

For more information, see the documentation and/or watch this video:

JuliaCon2020 Changing the immutable

Some creative usages of Setfield

VegaLite.jl overloads getproperty and lens API to manipulate JSON-based nested objects.

Kaleido.jl is a library of additional lenses.

PhaseSpaceIO.jl overloads getproperty and setproperties to get/set values from/in packed bits.

Download Details:

Author: jw3126
Source Code: https://github.com/jw3126/Setfield.jl 
License: View license

#julia #update 

Setfield.jl: Update Deeply Nested Immutable Structs

How to download and install Officejet pro 9000 all-in-one printer driv

If you are looking for a printer, which can help you in every possible ways with your office work, the Officejet 9000 can be the best model for you. It is packed with numerous features, and thus, you should get it. Here are all the facts, you should know about the printer. The HP Officejet 9000 is a multi-function printer, where you can get scanning facility, as well. It has wireless connectivity, which means, you can print out documents from anywhere and any device, you would like to. The printer is quite easy to use, which is another big reason, behind the popularity of the printer.Recently, a couple of clients utilizing the download and install Officejet pro 9000 all-in-one printer range have faced challenges while downloading the desired drivers. Assuming that you are additionally here with a similar reason this post is for you.

In the given article we have referenced various methods to download driver hp or update 123.hp.com/officejet 9000 for Windows 7, 8, 10, 11 devices. These driver updates not only improve the communication of the printer with your operating system but also enhance the speed of your Windows PC. Accordingly, go through the possible methods and apply the reasonable choice for Download and Install HP Printer drivers for Windows PC.

How to download and install Officejet pro 9000 drivers for Windows 7, 8, 10, 11?

If your printer has bugs, blank printing issues, or other errors download the latest hp officejet pro 9000 driver for Windows 7, 8, 10, or 11 devices to maintain the bridge between your PC and hardware device. No need to implement all the options, read the steps for each and apply the one that is suitable for you.

Method 1: Download and install Officejet 9000 Driver Update through Device Manager

If you have the time, there’s a built-in utility on your Windows devices that enables you to download HP Officejet 9000 drivers in a partly automatic way. Here’s how to use the utility for downloading HP Officejet 9000 driver for Windows 7, 8, 10, or 11 PCs or Laptops.

  1. Open the Run dialog box (Windows + R keys) and type devmgmt.msc. Press the Enter key on your keyboard to open Device Manager
  2. Click on the category Printers or Print Queues to expand. From the list locate and Right click on your HP Officejet 9000 driver.
  3. Select the alternative to Update Driver. In the following window select the first automatic search for the driver option.
  4. Double click on the driver to hp printer installation and Restart Windows PC to apply the update hp printer drivers.

Method 2: Use Bit Driver Updater for Automatic HP Oj 9000 Driver for Windows 7, 8, 10, 11?

Although there are numerous methods for HP Officejet 9000 driver download the automatic one tops our list. It simplifies the task to update drivers with automatic hp printer software download. The software can store the system specifications and quickly offer compatible and latest drivers for your device.

The Bit Driver Updater software updates HP Officejet 9000 driver and all the other drivers with a single click. Along with updating drivers the tool also empowers users to backup and restore the entire data in its huge driver database. Moreover, with the Pro update, it is easier to get technical assistance from the support team 24*7 regarding any relative concerns. You can perform quick scans and schedule driver updates with the help of this tool. All these features can be availed with Bit Driver Updater Pro which comes with a 60 day money back guarantee.

Here are the steps to be followed to download the software and use it for hp printer driver download.

  1. Click on the Download button to load the executable file for Bit Driver Updater. Double click on the file as the download completes and follow the instructions to install.
  2. Launch the hp officejet software and click on the Scan option on the left panel to start searching for updates.
  3. Wait till the command processes and the complete list of drivers with due updates is displayed.
  4. Locate HP Officejet 9000 driver update and click on the Update button present next to it.
  5. In addition, Pro version users can Update the entire list of drivers with a single click on the Update All button.
    Note: If you are using the Free version for Bit Driver updater click on the Update Now option for each driver to download one update at a time.
  6. Follow the instructions on your screen to install the latest version of the hp printer driver download for windows 10 devices.

Restart your Windows device to apply the updated driver software. The automatic driver updater software method for driver updates is the most convenient one. However, if you have the time and patience you can opt for the following method to hp officejet pro 9000 download.

Method 3: Download HP Officejet 9000 Driver Update from Official Website

Another and the most common method to download or update HP Officejet 9000 driver for Windows 11, 10, 8, or 7 devices is from the official website of HP. However, before you begin with the steps, find out the specifications of your system and its requirements to download the right drivers.

Open Windows Settings on your device and move to the about section. Check the Windows Edition and system type that are 9000 driver update.

  1. Visit the official support 123.hp setup.
  2. In the search bar write the model number of your printer and click the Submit button or enter key on your keyboard. In our case, it is HP Officejet 9000.
  3. Check your automatically detected Operating system version is correct and click on the Download button present next to the latest HP Officejet Driver update.
  4. As the download completes, double click on the driver file and apply the instructions on the screen to install.
  5. Restart your device to launch the HP Officejet 9000 driver update. This method is suitable only for the users who are skilled technically and have enough time & patience to hp printer drivers for windows 10 manually.

Conclusion:

The all-in-one printer series HP Officejet 9000 is supported by various Windows versions. We hope the guide proved to be useful in downloading the latest HP Officejet pro 9000 printer Drivers for your Windows devices. Although all the methods are reliable in our opinion automatic driver downloads through Bit Driver Updater is the simplest of all. Use the tool to update all the drivers at the ease of a single click.

tags 

#123.hp setup

#123.hp.com/officejet 9000

#Download and Install HP Printer drivers for Windows PC

#download driver hp

#hp officejet pro 9000 download

#HP Officejet pro 9000 printer Drivers

#hp officejet software

#hp printer driver download

 #hp printer drivers for windows 10

 #hp printer installation

#hp printer software download

 #hp printer software update

 #install Officejet 9000 printer driver

#update hp printer drivers

How to download and install Officejet pro 9000 all-in-one printer driv
Hong  Nhung

Hong Nhung

1660343400

Xây Dựng Một ứng Dụng đầy đủ Với Tetra

Hầu hết các ứng dụng full-stack tách mã frontend và backend thành các tệp riêng biệt; hầu hết các khuôn khổ web được xây dựng dựa trên cấu trúc này. Khi số lượng tệp và dòng mã tăng lên, nó có thể làm tăng độ phức tạp của cơ sở mã của bạn, do đó khiến việc gỡ lỗi thậm chí còn khó khăn hơn. Sự phức tạp gây ra bởi các tệp riêng biệt này đã được giảm thiểu thông qua việc giới thiệu một khuôn khổ có tên là Tetra .

Hướng dẫn này sẽ giới thiệu cho bạn về khung công tác Tetra và các thành phần của nó. Bạn cũng sẽ học cách xây dựng một ứng dụng blog đầy đủ đơn giản thực hiện các chức năng CRUD bằng Tetra.

Tetra là gì?

Tetra là một khung công tác đầy đủ được xây dựng với Django ở phía máy chủ và Alpine.js để thực hiện logic giao diện người dùng. Tetra cho phép bạn có logic frontend và backend ở một vị trí thống nhất và giảm độ phức tạp của mã trong ứng dụng của bạn. Nó kết nối việc triển khai phần phụ trợ với giao diện người dùng bằng cách sử dụng một lớp được gọi là lớp Thành phần.

Thành phần Tetra

Thành phần tetra là một đơn vị mã xử lý logic Python, HTML, CSS và JavaScript của nó như một thực thể trong một tệp Python duy nhất. Nếu bạn đã quen thuộc với React framework , bạn có thể ví hành vi của các thành phần của nó với các thành phần Tetra, ngoại trừ việc các thành phần React chỉ thực hiện các chức năng của giao diện người dùng.

Các thành phần có thể phụ thuộc hoặc độc lập với nhau. Điều này ngụ ý rằng bạn có thể gọi một thành phần này từ một thành phần khác hoặc có nó như một thành phần độc lập. Bạn có thể đọc thêm thông tin về thành phần tetra tại đây .

Hãy xây dựng một ứng dụng blog Tetra

Phần còn lại của hướng dẫn này sẽ hướng dẫn bạn cách cài đặt Tetra trong ứng dụng Django của bạn và quy trình từng bước về cách bạn tạo ứng dụng blog bằng Tetra. Ứng dụng blog sẽ được trình bày dưới góc độ quản trị viên, nơi bạn sẽ có thể tạo một bài đăng mới, cập nhật một bài đăng hiện có, xóa một bài đăng và xem tất cả các bài đăng trên blog.

Ứng dụng sẽ không bao gồm bất kỳ lớp xác thực hoặc ủy quyền nào. Mục đích là giữ cho nó càng đơn giản càng tốt trong khi tập trung vào các chức năng cốt lõi của Tetra.

Điều kiện tiên quyết

  • Thành thạo trong việc xây dựng các ứng dụng nguyên khối bằng Django
  • Kiến thức làm việc về HTML, CSS và JavaScript
  • Bất kỳ IDE hoặc trình soạn thảo văn bản phù hợp nào
  • Phiên bản Python 3.9 trở lên được cài đặt trên máy của bạn
  • trình quản lý gói npm được cài đặt trên máy của bạn

Thiết lập dự án

Bước đầu tiên là tạo môi trường ảo cho ứng dụng. Chạy lệnh sau trong thiết bị đầu cuối của bạn để thiết lập thư mục dự án và môi trường ảo của bạn:

mkdir tetra
cd tetra
python -m venv tetra 
cd tetra
Scripts/activate

Bước tiếp theo là cài đặt Django. Vì Tetra hoạt động trên khuôn khổ Django, nên bắt buộc phải tích hợp Django trong ứng dụng của bạn.

pip install django
django-admin startproject tetra_blog
cd tetra_blog

Tiếp theo, tạo ứng dụng blog:

python manage.py startapp blog

Thêm ứng dụng blog vào INSTALLED_APPSdanh sách trong settings.pytệp, như được hiển thị bên dưới:

INSTALLED_APPS = [
    'blog.apps.BlogConfig',
    ...
 ]

Trong thư mục ứng dụng, hãy tạo một components.pytệp chứa tất cả các thành phần mà bạn sẽ xây dựng trong dự án.

Cài đặt và cấu hình Tetra

Sau khi thiết lập thành công dự án Django, bước tiếp theo là cài đặt khung Tetra trong ứng dụng của bạn.

pip install tetraframework

Trong settings.pytệp, hãy thêm tetravào INSTALLED_APPSdanh sách, như được hiển thị bên dưới:

INSTALLED_APPS = [
    ...
    'tetra',
    'django.contrib.staticfiles',
    ...
]

Đảm bảo tetrađược liệt kê trước django.contrib.staticfilesphần tử.

Tiếp theo, bạn sẽ muốn thêm tetra.middleware.TetraMiddlewarevào cuối MIDDLEWAREdanh sách. Điều này sẽ thêm JavaScript và CSS từ thành phần của bạn vào mẫu HTML.

MIDDLEWARE = [ 
    ...
    'tetra.middleware.TetraMiddleware'
]

Áp dụng các sửa đổi dưới đây cho urls.pytệp gốc để hiển thị các điểm cuối của Tetra thông qua các phương pháp công khai của bạn:

from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    ...
 
    path('tetra/', include('tetra.urls')),
  ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

Cài đặt esbuild

Tetra xây dựng các thành phần JS / CSS của bạn và đóng gói chúng bằng cách sử dụng esbuild . Điều này cho phép bạn theo dõi bất kỳ lỗi nào có thể xảy ra khi triển khai giao diện người dùng đối với các tệp Python nguồn của bạn.

npm init
npm install esbuild

Nếu bạn đang sử dụng hệ điều hành Windows, bạn sẽ phải khai báo rõ ràng đường dẫn xây dựng esbuildtrong settings.pytệp của mình:

TETRA_ESBUILD_PATH = '<absolute-path-to-project-root-directory>/node_modules/.bin/esbuild.cmd'

Mô hình bài đăng trên blog

Ứng dụng sẽ thực hiện các chức năng CRUD trên một bài đăng trên blog. Mô Posthình sẽ bao gồm ba thuộc tính: tiêu đề, nội dung và ngày.

Thêm mã sau vào models.pytệp để thiết lập Postmô hình:

from django.db import models
from django.utils import timezone
from django.urls import reverse


class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    date_posted = models.DateTimeField(default=timezone.now)


    def __str__(self):
        return self.title
    // generate a reverse url for the model
    def get_absolute_url(self):
        return reverse('post-detail', kwargs={'pk': self.id})

Thực hiện các lệnh di chuyển để tạo bảng cho mô hình:

python manage.py makemigrations
python manage.py migrate

Thành AddPostphần

Thành phần này chịu trách nhiệm kết xuất giao diện người dùng để tạo một bài đăng mới. Nó cũng sẽ chứa logic Python mà chúng ta cần để tạo một Postmô hình và duy trì dữ liệu trong cơ sở dữ liệu.

Tạo add_post.pytệp trong componentsthư mục và thêm mã sau vào tệp:

from sourcetypes import javascript, css, django_html
from tetra import Component, public, Library
from ..models import Post

default = Library()

@default.register
class AddPost(Component):
    title=public("")
    content=public("")

 
    def load(self):
        self.post = Post.objects.filter(id=0)


    @public
    def add_post(self, title, content):
        post = Post(
            title = title,
            content = content
        )
        post.save()

Trong đoạn mã trên, AddPostlớp này là lớp con của lớp thành phần Tetra, là lớp cơ sở mà bạn xây dựng các thành phần tùy chỉnh của mình. Sử dụng trình @default.registertrang trí, bạn đăng ký AddPostthành phần của mình vào thư viện Tetra.

Các titlecontentbiến là các thuộc tính công khai của thành phần, mỗi thuộc tính có giá trị ban đầu là một chuỗi rỗng. Các giá trị của public attributescó sẵn cho các mẫu, JavaScript và logic máy chủ.

Phương loadthức này chạy khi thành phần khởi chạy và khi nó tiếp tục từ trạng thái đã lưu. Bạn có thể coi loadphương thức là phương thức khởi tạo của thành phần; nó chạy khi bạn gọi thành phần từ một mẫu.

Phương add_postthức là một phương thức công khai nhận tiêu đề và nội dung làm đối số để tạo một Postthể hiện và sau đó lưu nó vào cơ sở dữ liệu. Cũng giống như các thuộc tính công khai, các phương thức công khai được hiển thị với mẫu, JavaScript và Python. Bạn khai báo một phương thức là công khai bằng cách thêm trình @publictrang trí phía trên chữ ký phương thức.

Đây là mã HTML bạn nên đưa vào add_post.pytệp như một phần của AddPostthành phần:

template: django_html = """
   
    <div class="container">
        <h2>Add blog post</h2>
        <label> Title
        <em>*</em>
        </label>
        <input type="text" maxlength="255" x-model="title" name="title" placeholder="Input post title" required/>

        <label> Content
        <em>*</em>
        </label>
        <textarea rows="20" cols="80" x-model="content" name="content" placeholder="Input blog content" required /> </textarea>

        <button type="submit" @click="addPost(title, content)"><em>Submit</em></button>
    </div>
    
    """

Trường đầu vào nhận tiêu đề bài đăng và liên kết nó với titlethuộc tính public thông qua thuộc tính Alpine.js x-model . Tương tự như vậy, hàm textareanhận nội dung của bài đăng trên blog và liên kết giá trị với contentthuộc tính công khai của thành phần.

Sử dụng lệnh Alpine.js @clicktrong thẻ nút, mẫu gọi addPostphương thức JavaScript:

script: javascript = """
    export default {

        addPost(title, content){
            this.add_post(title, content)   
        }
        
    }
    """

Phương thức JavaScript addPostchuyển các giá trị thu được từ tiêu đề và nội dung làm đối số cho add_postphương thức công khai của thành phần. Bạn cũng có thể gọi add_postphương thức công khai trực tiếp từ mẫu HTML ở trên.

Mục đích của việc chuyển nó qua hàm JavaScript ở đây là để chứng minh cách bạn thực hiện một thao tác JavaScript trong thành phần Tetra của mình. Điều này hữu ích cho các tình huống mà bạn muốn kiểm soát nhiều hơn hành vi của người dùng, chẳng hạn như có khả năng tắt một nút sau khi người dùng đã nhấp vào nút đó để ngăn họ gửi nhiều yêu cầu trong khi xử lý các yêu cầu trước đó.

Đây là mã CSS để tạo kiểu cho mẫu:

style: css = """
    .container {
        display: flex;
        flex-direction: column;
        align-items: left;
        justify-content: center;
        border-style: solid;
        width: fit-content;
        margin: auto;
        margin-top: 50px;
        width: 50%;
        border-radius: 15px;
        padding: 30px;
    }

    input, textarea, label{
        margin-bottom: 30px;
        margin-left: 20px;
        ;
    }

    label {
        font-weight: bold;
    }

    input{
        height: 40px;
    }

    h2 {
        text-align: center;
    }

    button {
        width: 150px;
        padding: 10px;
        border-radius: 9px;
        border-style: none;
        background: green;
        color: white;
        margin: auto;
    }
    
    """

Bước tiếp theo là gọi AddPostthành phần từ mẫu chế độ xem Django. Tạo add_post.htmltệp trong thư mục ứng dụng blog templatesmà bạn đã tạo trong phần trước của hướng dẫn này. Thêm đoạn mã sau vào tệp:

{% load tetra %}
<!Doctype html>
<html lang="en">
  <head>
    <title> Add post </title>
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
     <form enctype="multipart/form-data" method="POST">
      {% csrf_token %}
      {% @ add_post / %}
    </form>
  </body>
  </html>

Mẫu này bắt đầu bằng cách tải các thẻ mẫu Tetra vào mẫu. Nó đạt được điều này thông qua {% load tetra %}lệnh được mô tả ở đầu mã. Bạn cũng sẽ cần đưa CSS và JavaScript vào mẫu thông qua {% tetra_styles %}{% tetra_scripts}tương ứng.

Theo mặc định, Tetra không bao gồm Alpine.js trong mẫu của bạn. Bạn phải khai báo rõ ràng nó bằng cách thêm include_alpine=Truekhi chèn JavaScript của thành phần.

Thẻ {% @ add_post / %}bên trong formthẻ gọi loadphương thức của AddPostthành phần và hiển thị nội dung HTML mà bạn đã khai báo ở trên khi tạo thành phần.

Lưu ý rằng tên thành phần được sử dụng để tải thành phần là trong trường hợp rắn. Đây là cấu hình mặc định để gọi các thành phần từ các mẫu. Bạn cũng có thể đặt tên tùy chỉnh khi tạo thành phần, như được hiển thị bên dưới:

...
@default.register(name="custom_component_name")
class AddPost(Component):
...

Sau đó, bạn có thể tải thành phần bằng cách sử dụng {% @ custom_component_name / %}.

Tiếp theo, thêm đoạn mã dưới đây vào views.pytệp:

from django.shortcuts import render

def add_post(request):
    return render(request, 'add_post.html')

Tạo một urls.pytệp trong thư mục ứng dụng blog và thêm đoạn mã sau vào tệp:

from django.urls import path
from . import views


urlpatterns = [
     path("add", views.add_post, name='add-post'),

]

Trong urls.pytệp gốc, thêm đường dẫn dưới đây:

urlpatterns = [
    ...
    path('tetra/', include('tetra.urls')),
    path('post/', include('blog.urls'))
]

Chạy ứng dụng với python manage.py runserver command. Xem trang trên trình duyệt của bạn thông qua localhost:8000/post/add.

Đây là đầu ra của trang:

Thêm trang bài đăng blog với hộp tiêu đề và hộp nội dung

Thành PostItemphần

Thành PostItemphần chứa mẫu để hiển thị một bài đăng đã tạo trên màn hình chính.

@default.register
class PostItem(Component):
    
    def load(self, post):
        self.post = post

Phương loadthức nhận Postcá thể làm đối số của nó và hiển thị nó với mẫu HTML hiển thị tiêu đề và nội dung của nó trên màn hình.

 template: django_html = """
   
    <article class="post-container" {% ... attrs %}>
            <small class="article-metadata">{{ post.date_posted.date}}</small>
            <p class="article-title"> {{ post.title }}</p>
            <p class="article-content">{{ post.content }}</p>

        </article>
            
    """

Thẻ {% ... attrs %}là một thẻ thuộc tính Tetra mà mẫu sử dụng để nhận các đối số được truyền cho nó khi gọi thành phần. Khi nhận các đối số bằng thẻ thuộc tính, bạn nên khai báo thẻ trong nút gốc của mẫu HTML, như được thực hiện trong thẻ bài viết trong đoạn mã ở trên.

Đây là cách triển khai CSS của mẫu:

style: css = """
    
    .article-metadata {
        padding-bottom: 1px;
        margin-bottom: 4px;
        border-bottom: 1px solid #e3e3e3;
        
    }


    .article-title{
        font-size: x-large;
        font-weight: 700;
    }

    .article-content {
        white-space: pre-line;
    }

    .post-container{
        margin: 50px;
    }

    a.article-title:hover {
        color: #428bca;
        text-decoration: none;
    }

    .article-content {
        white-space: pre-line;
    }

    a.nav-item{
        text-align: right;
        margin-right: 100px;
    }

    h1 {
       text-align: center;
    }
    """

Đây là một bài đăng sẽ trông như thế nào thông qua PostItemthành phần:

Thành phần PostItem hiển thị bài đăng với văn bản mặc định

Thành ViewPostsphần

Thành phần này chịu trách nhiệm hiển thị tất cả các bài viết đã tạo. Thêm đoạn mã sau vào components.pytệp:

@default.register
class PostView(Component):

    def load(self):
        self.posts = Post.objects.all()

    template: django_html = """
        <div>
            <h1> Tetra blog </h1>
            <div class="navbar-nav">
                <a class="nav-item nav-link" href="{% url 'add-post' %}">New Post</a>
            <div>
            <div class="list-group">
                {% for post in posts %}
                    {% @ post_item post=post key=post.id / %}
                {% endfor %}
            </div>
         </div>
        """

Phương loadthức trong các thành phần truy xuất tất cả các bài đăng đã tạo từ cơ sở dữ liệu. Mẫu HTML chứa một thẻ liên kết hướng đến add-postURL để tạo một bài đăng mới.

Đối với mỗi bài đăng được tìm nạp từ cơ sở dữ liệu, HTML tạo một PostItemthành phần bằng cách chuyển đối tượng bài đăng làm đối số của nó trong vòng lặp for.

Tiếp theo, gọi ViewPostthành phần từ mẫu chế độ xem Django. Tạo một home.htmltệp trong thư mục của ứng dụng blog templatesvà thêm đoạn mã sau vào tệp:

{% load tetra %}
<!Doctype html>
<html lang="en">
  <head>
    <title> Blog home </title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
      {% @ view_post / %}
  </body>
  </html>

Tiếp theo, thêm phần sau vào views.pytệp:

def home(request):
    return render(request, 'home.html')

Cuối cùng, cập nhật urlpatternsdanh sách trong tệp ứng dụng blog urls.py.

urlpatterns = [
     path("", views.home, name='home'),
    ...
]

Bạn có thể xem trang qua localhost:8000/post.

Blog Tetra

Thành PostDetailphần

Thành phần này sẽ hiển thị toàn bộ bài đăng trên một trang duy nhất. Trang cũng sẽ chứa hai nút: mỗi nút để xóa và cập nhật bài đăng. Thêm mã sau vào components.pytệp:

@default.register
class PostDetail(Component):
 
    def load(self, pk):
        self.post = Post.objects.filter(id=pk)[0]

    @public(update=False)
    def delete_item(self):
        Post.objects.filter(id=self.post.id).delete()
        self.client._removeComponent()

Phương loadthức nhận idbài đăng thông qua pkbiến và tìm nạp Postcá thể có ID khớp với pkgiá trị từ cơ sở dữ liệu.

Phương delete_itempháp này xóa Postcá thể khỏi cơ sở dữ liệu, tự động xóa nó khỏi màn hình chính. Theo mặc định, một phương thức công khai sẽ hiển thị lại một thành phần khi bạn gọi nó. Bằng cách đặt thuộc updatetính thành Falsetrong trình @publictrang trí, nó đảm bảo rằng nó không cố gắng hiển thị lại bài đăng đã xóa trước đó.

Đây là mẫu HTML:

 template: django_html = """
        <article > 
            <small class="text-muted">{{ post.date_posted.date}}</small>
                
            <h2 class="article-title">{{ post.title }}</h2>
            <p class="article-content">{{ post.content }}</p>

            <div class="post-buttons">
            <button id="delete-button" type="submit" @click="delete_item()"><em>Delete</em></button>
           <button id="update-button"> <em>Update</em> </button>
            </div>
           
        </article>
    """

Mẫu truy xuất ngày, tiêu đề và nội dung của bài đăng được tìm nạp từ loadphương thức và hiển thị các giá trị này. Nó cũng chứa các nút để xóa và cập nhật bài đăng. Nút Xóa gọi delete_itemphương thức để thực hiện thao tác xóa trên bài đăng. Chúng tôi sẽ triển khai nút Cập nhật trong phần tiếp theo.

Đây là CSS cho mẫu:

 style: css = """

        article{
            margin: 100px;
        }

        .post-buttons{
            position: absolute;
            right: 0;
        }

        #delete-button, #update-button{
            width: 150px;
            padding: 10px;
            border-radius: 9px;
            border-style: none;
            font-weight: bold;
            margin: auto;
        }

        #update-button{
            background: blue;
            color: white;
        }

        #delete-button{
            background: red;
            color: white;
        }

    """

Trong PostItemmẫu được tạo ở phần trước, hãy cập nhật mã HTML bằng cách đưa vào một anchorthẻ sẽ hướng người dùng đến trang chi tiết bài đăng từ màn hình chính.

@default.register
class PostItem(Component):
   
  ...        
   
    template: django_html = """
   
    <article class="post-container" >
           ...
            <a href="{% url 'post-detail' pk=post.id %}"> {{ post.title }}</a>
          ...

        </article>
            
    """

Trong thư mục mẫu, hãy tạo một post-detail.htmltệp sẽ đóng vai trò là tệp HTML gốc cho trang chi tiết và bao gồm mã sau trong tệp:

Tiếp theo, cập nhật tệp views.pyurls.pytệp để bao gồm đường dẫn đến trang chi tiết:

def post_detail(request, **kwargs):
    return render(request, 'post_detail.html', kwargs)urlpatterns = [
     path("<int:pk>/", views.post_detail, name='post-detail')
]

Xem chi tiết bài đăng trong trình duyệt của bạn bằng cách nhấp vào tiêu đề bài đăng từ trang chủ blog.

Đăng chi tiết bằng các nút xóa và cập nhật

Thành UpdatePostphần

Thành phần này có nhiệm vụ cập nhật tiêu đề và nội dung của một bài đăng hiện có.

@default.register
class PostUpdate(Component):
    title=public("")
    content=public("")


    def load(self, pk):
        self.post = Post.objects.filter(id=pk)[0]
        self.title=self.post.title
        self.content=self.post.content

    @public
    def update_post(self, title, content):
        self.post.title = title
        self.post.content = content

        self.post.save()

Phương loadthức nhận ID của bài đăng bạn muốn cập nhật và tìm nạp nó từ cơ sở dữ liệu. Sau đó, nó gán tiêu đề và nội dung của nó cho các thuộc tính titlecontentthuộc tính công khai tương ứng.

Phương update_postthức nhận tiêu đề và nội dung được cập nhật và gán chúng vào bài đăng được tải xuống, sau đó lưu nó vào cơ sở dữ liệu.

Dưới đây là mẫu HTML của thành phần:

 template: django_html = """
        <div class="container">
            <h2>Update blog post</h2>
            <label> Title
            <em>*</em>
            </label>
            <input type="text" maxlength="255" x-model="title" name="title" required/>

            <label> Content
            <em>*</em>
            </label>
            <textarea rows="20" cols="80" x-model="content" name="content" required> </textarea>

            <button type="submit" @click="update_post(title, content)"><em>Submit</em></button>
        </div>
        """

Mẫu trên hiển thị giá trị của tiêu đề và thuộc tính công khai nội dung thông qua thuộc tính Alpine.js x-model, trong khi nút sử dụng @clickhàm Alpine.js để gọi update_postphương thức và chuyển giá trị mới của tiêu đề và nội dung làm đối số.

Trong PostDetailmẫu đã tạo ở phần trước, hãy cập nhật mã HTML bằng cách thêm một anchorthẻ sẽ hướng người dùng đến trang cập nhật bài đăng từ màn hình chính.

@default.register
class PostDetail(Component):
 
   ...

    template: django_html = """
        <article  {% ... attrs %} > 
            ...
            <a class="nav-item nav-link" href="{% url 'update-post' pk=post.id %}"><button id="update-button"> <em>Update</em> </button></a>
         ...
           
        </article>
    """

Tiếp theo, trong thư mục mẫu, hãy tạo một post_update.htmltệp sẽ đóng vai trò là mẫu HTML gốc cho PostUpdatethành phần. Thêm đoạn mã sau vào tệp:

{% load tetra %}
<!Doctype html>
<html>
  <head>
    <title> Update post </title>
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
     <form enctype="multipart/form-data" method="POST">
      {% csrf_token %}
      {% @ post_update pk=pk / %}
    </form>
  </body>
  </html>

Cuối cùng, cập nhật tệp views.pyurls.pytệp với mã sau tương ứng:

def update_post(request, **kwargs):
    return render(request, 'post_update.html', kwargs)urlpatterns = [
     ...
     path("<int:pk>", views.update_post, name='update-post'),
     ...

]

Bạn có thể điều hướng đến update-posttrang bằng cách nhấp vào nút Cập nhật trên màn hình chi tiết bài đăng.

Cập nhật màn hình bài đăng blog với các hộp tiêu đề và nội dung

Tiêu đề bài đăng trên blog hiện hiển thị "-cập nhật"

Ghi chú về sự sẵn sàng sản xuất của Tetra

Tại thời điểm viết bài này, Tetra vẫn đang trong giai đoạn phát triển ban đầu và hiện hỗ trợ Python 3.9 trở lên. Tuy nhiên, nhóm Tetra đang làm việc để mở rộng các chức năng của khung công tác này sang các phiên bản cũ hơn của Python.

Một điều bạn nên biết trước khi bắt đầu sản xuất với Tetra là tài liệu khung cần được cải thiện rất nhiều. Nó quá ngắn gọn, vì một số phụ thuộc hoặc không được giải thích ở tất cả hoặc không đủ chi tiết. Ví dụ: tài liệu không thảo luận về cách xử lý hình ảnh, đó là lý do tại sao chúng tôi xây dựng ứng dụng blog cho bản demo này.

Mãi cho đến khi tôi hoàn thành dự án, tôi mới nhận ra rằng khung công tác không phức tạp như tài liệu đã trình bày.

Sự kết luận

Bài viết này đã giới thiệu cho bạn về Tetra và các thành phần của nó. Bạn đã học cách Tetra hoạt động và thực hiện các hoạt động toàn ngăn xếp từ một tệp duy nhất bằng cách xây dựng một ứng dụng blog đơn giản thực hiện các hoạt động CRUD.

Trang chủ Tetra chứa một số ví dụ bổ sung về cách bạn có thể tạo một số ứng dụng đơn giản với Tetra. Nếu bạn cũng muốn tìm hiểu thêm về khuôn khổ này, tài liệu có sẵn để hướng dẫn bạn. Bạn có thể kiểm tra việc triển khai đầy đủ ứng dụng blog trên GitHub .

Nguồn: https://blog.logrocket.com/build-full-stack-app-tetra/

 #tetra  #fullstack #python 

Xây Dựng Một ứng Dụng đầy đủ Với Tetra
Mélanie  Faria

Mélanie Faria

1660341600

Crie Um Aplicativo Full-stack Com Tetra

A maioria dos aplicativos full-stack separa o código de front-end e back-end em arquivos distintos; a maioria dos frameworks web são construídos com base nessa estrutura. À medida que o número de arquivos e linhas de código aumenta, pode aumentar a complexidade de sua base de código, dificultando ainda mais a depuração. A complexidade causada por esses arquivos separados foi minimizada com a introdução de um framework chamado Tetra .

Este tutorial apresentará a estrutura Tetra e seus componentes. Você também aprenderá a construir um aplicativo de blog full-stack simples que executa funcionalidades CRUD usando Tetra.

O que é Tetra?

Tetra é um framework full-stack construído com Django no lado do servidor e Alpine.js para executar a lógica de frontend. O Tetra permite que você tenha lógica de front-end e back-end em um local unificado e reduz a complexidade do código em seu aplicativo. Ele conecta a implementação de back-end com o front-end usando uma classe conhecida como classe Component.

O componente Tetra

Um componente tetra é uma unidade de código que lida com sua lógica Python, HTML, CSS e JavaScript como uma entidade em um único arquivo Python. Se você estiver familiarizado com o framework React , você pode comparar o comportamento de seus componentes com os componentes Tetra, exceto que os componentes React executam apenas as funcionalidades de front-end.

Os componentes podem ser dependentes ou independentes uns dos outros. Isso implica que você pode invocar um componente de outro ou tê-lo como um componente autônomo. Você pode ler mais informações sobre o componente tetra aqui .

Vamos construir um aplicativo de blog Tetra

O restante deste tutorial irá guiá-lo através de como instalar o Tetra em seu aplicativo Django e um fluxo passo a passo de como você construiria um aplicativo de blog usando o Tetra. O aplicativo de blog será apresentado de uma perspectiva de administrador, onde você poderá criar uma nova postagem, atualizar uma postagem existente, excluir uma postagem e visualizar todas as postagens do blog.

O aplicativo não incluirá nenhuma camada de autenticação ou autorização. O objetivo é mantê-lo o mais simples possível, concentrando-se nas principais funcionalidades do Tetra.

Pré-requisitos

  • Proficiência na construção de aplicativos monolíticos usando Django
  • Conhecimento prático de HTML, CSS e JavaScript
  • Qualquer IDE ou editor de texto adequado
  • Python versão 3.9 ou superior instalado em sua máquina
  • gerenciador de pacotes npm instalado em sua máquina

Configuração do projeto

O primeiro passo é criar um ambiente virtual para o aplicativo. Execute o seguinte comando em seu terminal para configurar o diretório do projeto e o ambiente virtual:

mkdir tetra
cd tetra
python -m venv tetra 
cd tetra
Scripts/activate

O próximo passo é instalar o Django. Como o Tetra opera no framework Django, é necessário integrar o Django em seu aplicativo.

pip install django
django-admin startproject tetra_blog
cd tetra_blog

Em seguida, crie o aplicativo de blog:

python manage.py startapp blog

Adicione o aplicativo de blog à INSTALLED_APPSlista no settings.pyarquivo, conforme mostrado abaixo:

INSTALLED_APPS = [
    'blog.apps.BlogConfig',
    ...
 ]

Dentro do diretório do aplicativo, crie um components.pyarquivo que conterá todos os componentes que você construirá no projeto.

Instalação e configuração do Tetra

Após configurar com sucesso o projeto Django, a próxima etapa é instalar o framework Tetra em seu aplicativo.

pip install tetraframework

No settings.pyarquivo, adicione tetraà INSTALLED_APPSlista, conforme mostrado abaixo:

INSTALLED_APPS = [
    ...
    'tetra',
    'django.contrib.staticfiles',
    ...
]

Certifique tetra-se de que esteja listado antes do django.contrib.staticfileselemento.

Em seguida, você desejará incluir tetra.middleware.TetraMiddlewareno final da MIDDLEWARElista. Isso adiciona o JavaScript e o CSS do seu componente ao modelo HTML.

MIDDLEWARE = [ 
    ...
    'tetra.middleware.TetraMiddleware'
]

Aplique as modificações abaixo ao arquivo raiz urls.pypara expor os endpoints da Tetra por meio de seus métodos públicos:

from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    ...
 
    path('tetra/', include('tetra.urls')),
  ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

Instalar esbuild

A Tetra constrói seus componentes JS/CSS e os empacota usando esbuild . Isso permite que você rastreie quaisquer erros que possam ocorrer na implementação de front-end para seus arquivos Python de origem.

npm init
npm install esbuild

Se você estiver usando o sistema operacional Windows, precisará declarar explicitamente o caminho de compilação esbuildem seu settings.pyarquivo:

TETRA_ESBUILD_PATH = '<absolute-path-to-project-root-directory>/node_modules/.bin/esbuild.cmd'

Modelo de postagem do blog

O aplicativo executará funções CRUD em uma postagem de blog. O Postmodelo compreenderá três atributos: título, conteúdo e data.

Adicione o seguinte código ao models.pyarquivo para configurar o Postmodelo:

from django.db import models
from django.utils import timezone
from django.urls import reverse


class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    date_posted = models.DateTimeField(default=timezone.now)


    def __str__(self):
        return self.title
    // generate a reverse url for the model
    def get_absolute_url(self):
        return reverse('post-detail', kwargs={'pk': self.id})

Execute os comandos de migração para criar uma tabela para o modelo:

python manage.py makemigrations
python manage.py migrate

O AddPostcomponente

Este componente é responsável por renderizar a UI para criar uma nova postagem. Ele também conterá a lógica do Python que precisamos para criar um Postmodelo e persistir os dados no banco de dados.

Crie add_post.pyo arquivo na componentspasta e adicione o seguinte código ao arquivo:

from sourcetypes import javascript, css, django_html
from tetra import Component, public, Library
from ..models import Post

default = Library()

@default.register
class AddPost(Component):
    title=public("")
    content=public("")

 
    def load(self):
        self.post = Post.objects.filter(id=0)


    @public
    def add_post(self, title, content):
        post = Post(
            title = title,
            content = content
        )
        post.save()

No código acima, a AddPostclasse é uma subclasse da classe do componente Tetra, que é a classe base sobre a qual você constrói seus componentes personalizados. Usando o @default.registerdecorador, você registra seu AddPostcomponente na biblioteca Tetra.

As variáveis title​​e são atributos públicos do componente, cada um com um valor inicial de uma string vazia. Os valores de estão disponíveis para os modelos, JavaScript e lógica do servidor.contentpublic attributes

O loadmétodo é executado quando o componente é iniciado e quando é retomado de um estado salvo. Você pode pensar no loadmétodo como o construtor do componente; ele é executado quando você chama o componente de um modelo.

O add_postmétodo é um método público que recebe o título e o conteúdo como argumentos para criar uma Postinstância e depois salva no banco de dados. Assim como os atributos públicos, os métodos públicos são expostos ao modelo, JavaScript e Python. Você declara um método como público adicionando o @publicdecorador acima da assinatura do método.

Aqui está o código HTML que você deve incluir no add_post.pyarquivo como parte do AddPostcomponente:

template: django_html = """
   
    <div class="container">
        <h2>Add blog post</h2>
        <label> Title
        <em>*</em>
        </label>
        <input type="text" maxlength="255" x-model="title" name="title" placeholder="Input post title" required/>

        <label> Content
        <em>*</em>
        </label>
        <textarea rows="20" cols="80" x-model="content" name="content" placeholder="Input blog content" required /> </textarea>

        <button type="submit" @click="addPost(title, content)"><em>Submit</em></button>
    </div>
    
    """

O campo de entrada recebe o título do post e o vincula ao titleatributo public por meio da propriedade x-model Alpine.js . Da mesma forma, o textarearecebe o conteúdo da postagem do blog e vincula o valor ao contentatributo public do componente.

Usando a diretiva Alpine.js @clickna tag de botão, o modelo invoca o addPostmétodo JavaScript:

script: javascript = """
    export default {

        addPost(title, content){
            this.add_post(title, content)   
        }
        
    }
    """

O método JavaScript addPostpassa os valores obtidos do título e do conteúdo como argumentos para o add_postmétodo público do componente. Você também pode invocar o add_postmétodo público diretamente do modelo HTML acima.

O objetivo de passá-lo pela função JavaScript aqui é demonstrar como você realizaria uma operação JavaScript em seu componente Tetra. Isso é útil para situações em que você deseja ter mais controle sobre o comportamento do usuário, como desabilitar potencialmente um botão após o usuário clicar nele para impedir que ele envie várias solicitações durante o processamento das anteriores.

Aqui está o código CSS para estilizar o modelo:

style: css = """
    .container {
        display: flex;
        flex-direction: column;
        align-items: left;
        justify-content: center;
        border-style: solid;
        width: fit-content;
        margin: auto;
        margin-top: 50px;
        width: 50%;
        border-radius: 15px;
        padding: 30px;
    }

    input, textarea, label{
        margin-bottom: 30px;
        margin-left: 20px;
        ;
    }

    label {
        font-weight: bold;
    }

    input{
        height: 40px;
    }

    h2 {
        text-align: center;
    }

    button {
        width: 150px;
        padding: 10px;
        border-radius: 9px;
        border-style: none;
        background: green;
        color: white;
        margin: auto;
    }
    
    """

O próximo passo é invocar o AddPostcomponente do template de view do Django. Crie um add_post.htmlarquivo na pasta do aplicativo de blog templatesque você criou na seção anterior deste tutorial. Adicione o seguinte trecho ao arquivo:

{% load tetra %}
<!Doctype html>
<html lang="en">
  <head>
    <title> Add post </title>
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
     <form enctype="multipart/form-data" method="POST">
      {% csrf_token %}
      {% @ add_post / %}
    </form>
  </body>
  </html>

Este modelo começa carregando as tags de modelo Tetra no modelo. Ele consegue isso através do {% load tetra %}comando descrito na parte superior do código. Você também precisará injetar o CSS e o JavaScript no modelo por meio de {% tetra_styles %}e {% tetra_scripts}, respectivamente.

Por padrão, Tetra não inclui Alpine.js em seu modelo. Você precisa declará-lo explicitamente adicionando include_alpine=Trueao injetar o JavaScript do componente.

A {% @ add_post / %}tag dentro da formtag invoca o loadmétodo do AddPostcomponente e renderiza o conteúdo HTML que você declarou acima ao criar o componente.

Observe que o nome do componente usado para carregar o componente está em maiúsculas. Esta é a configuração padrão para chamar componentes de modelos. Você também pode definir um nome personalizado ao criar o componente, conforme mostrado abaixo:

...
@default.register(name="custom_component_name")
class AddPost(Component):
...

Então você pode carregar o componente usando {% @ custom_component_name / %}.

Em seguida, adicione o snippet abaixo ao views.pyarquivo:

from django.shortcuts import render

def add_post(request):
    return render(request, 'add_post.html')

Crie um urls.pyarquivo no diretório do aplicativo do blog e adicione o seguinte snippet ao arquivo:

from django.urls import path
from . import views


urlpatterns = [
     path("add", views.add_post, name='add-post'),

]

No arquivo raiz urls.py, adicione o caminho abaixo:

urlpatterns = [
    ...
    path('tetra/', include('tetra.urls')),
    path('post/', include('blog.urls'))
]

Execute o aplicativo com python manage.py runserver command. Visualize a página no seu navegador através do localhost:8000/post/add.

Aqui está a saída da página:

Adicionar página de postagem de blog com caixa de título e caixa de conteúdo

O PostItemcomponente

O PostItemcomponente contém o modelo para renderizar uma postagem criada na tela inicial.

@default.register
class PostItem(Component):
    
    def load(self, post):
        self.post = post

O loadmétodo recebe a Postinstância como seu argumento e a expõe ao template HTML que renderiza seu título e conteúdo na tela.

 template: django_html = """
   
    <article class="post-container" {% ... attrs %}>
            <small class="article-metadata">{{ post.date_posted.date}}</small>
            <p class="article-title"> {{ post.title }}</p>
            <p class="article-content">{{ post.content }}</p>

        </article>
            
    """

A {% ... attrs %}tag é uma tag de atributo Tetra que o template usa para receber os argumentos passados ​​para ele ao chamar o componente. Ao receber argumentos utilizando a tag de atributos, você deve declarar a tag no nó raiz do template HTML, conforme feito na tag de artigo no snippet acima.

Aqui está a implementação CSS do modelo:

style: css = """
    
    .article-metadata {
        padding-bottom: 1px;
        margin-bottom: 4px;
        border-bottom: 1px solid #e3e3e3;
        
    }


    .article-title{
        font-size: x-large;
        font-weight: 700;
    }

    .article-content {
        white-space: pre-line;
    }

    .post-container{
        margin: 50px;
    }

    a.article-title:hover {
        color: #428bca;
        text-decoration: none;
    }

    .article-content {
        white-space: pre-line;
    }

    a.nav-item{
        text-align: right;
        margin-right: 100px;
    }

    h1 {
       text-align: center;
    }
    """

Aqui está a aparência de uma postagem por meio do PostItemcomponente:

O componente PostItem mostra a postagem com texto padrão

O ViewPostscomponente

Este componente é responsável por renderizar todos os posts criados. Adicione o seguinte trecho ao components.pyarquivo:

@default.register
class PostView(Component):

    def load(self):
        self.posts = Post.objects.all()

    template: django_html = """
        <div>
            <h1> Tetra blog </h1>
            <div class="navbar-nav">
                <a class="nav-item nav-link" href="{% url 'add-post' %}">New Post</a>
            <div>
            <div class="list-group">
                {% for post in posts %}
                    {% @ post_item post=post key=post.id / %}
                {% endfor %}
            </div>
         </div>
        """

O loadmétodo nos componentes recupera todas as postagens criadas do banco de dados. O modelo HTML contém uma tag âncora que direciona para a add-postURL para criar uma nova postagem.

Para cada post buscado no banco de dados, o HTML cria um PostItemcomponente passando o objeto post como seu argumento dentro do loop for.

Em seguida, invoque o ViewPostcomponente do modelo de visualização do Django. Crie um home.htmlarquivo na pasta do aplicativo de blog templatese adicione o seguinte snippet ao arquivo:

{% load tetra %}
<!Doctype html>
<html lang="en">
  <head>
    <title> Blog home </title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
      {% @ view_post / %}
  </body>
  </html>

Em seguida, adicione o seguinte ao views.pyarquivo:

def home(request):
    return render(request, 'home.html')

Por fim, atualize a urlpatternslista no arquivo do aplicativo do blog urls.py.

urlpatterns = [
     path("", views.home, name='home'),
    ...
]

Você pode visualizar a página via localhost:8000/post.

Blog Tetra

O PostDetailcomponente

Este componente renderizará a postagem completa em uma única página. A página também conterá dois botões: um para excluir e atualizar a postagem. Adicione o seguinte código ao components.pyarquivo:

@default.register
class PostDetail(Component):
 
    def load(self, pk):
        self.post = Post.objects.filter(id=pk)[0]

    @public(update=False)
    def delete_item(self):
        Post.objects.filter(id=self.post.id).delete()
        self.client._removeComponent()

O loadmétodo recebe o idda postagem por meio da pkvariável e busca a Postinstância, cujo ID corresponde ao pkvalor do banco de dados.

O delete_itemmétodo exclui a Postinstância do banco de dados, removendo-a automaticamente da tela inicial. Por padrão, um método público renderizará novamente um componente quando você o invocar. Ao definir a updatepropriedade como Falseno @publicdecorador, ele garante que não tente renderizar novamente uma postagem excluída anteriormente.

Aqui está o modelo HTML:

 template: django_html = """
        <article > 
            <small class="text-muted">{{ post.date_posted.date}}</small>
                
            <h2 class="article-title">{{ post.title }}</h2>
            <p class="article-content">{{ post.content }}</p>

            <div class="post-buttons">
            <button id="delete-button" type="submit" @click="delete_item()"><em>Delete</em></button>
           <button id="update-button"> <em>Update</em> </button>
            </div>
           
        </article>
    """

O modelo recupera a data, o título e o conteúdo da postagem obtida do loadmétodo e renderiza esses valores. Ele também contém botões para excluir e atualizar a postagem. O botão Excluir invoca o delete_itemmétodo para executar a operação de exclusão na postagem. Implementaremos o botão Atualizar na seção subsequente.

Aqui está o CSS para o modelo:

 style: css = """

        article{
            margin: 100px;
        }

        .post-buttons{
            position: absolute;
            right: 0;
        }

        #delete-button, #update-button{
            width: 150px;
            padding: 10px;
            border-radius: 9px;
            border-style: none;
            font-weight: bold;
            margin: auto;
        }

        #update-button{
            background: blue;
            color: white;
        }

        #delete-button{
            background: red;
            color: white;
        }

    """

No PostItemmodelo criado na seção anterior, atualize o código HTML incluindo uma anchortag que direcionará o usuário para a página de detalhes do post na tela inicial.

@default.register
class PostItem(Component):
   
  ...        
   
    template: django_html = """
   
    <article class="post-container" >
           ...
            <a href="{% url 'post-detail' pk=post.id %}"> {{ post.title }}</a>
          ...

        </article>
            
    """

Na pasta de modelos, crie um post-detail.htmlarquivo que servirá como o arquivo HTML raiz para a página de pós-detalhe e inclua o seguinte código no arquivo:

Em seguida, atualize os arquivos views.pye urls.pypara incluir o caminho para a página de pós-detalhe:

def post_detail(request, **kwargs):
    return render(request, 'post_detail.html', kwargs)urlpatterns = [
     path("<int:pk>/", views.post_detail, name='post-detail')
]

Visualize os detalhes da postagem em seu navegador clicando no título da postagem na página inicial do blog.

Postar detalhes com botões excluir e atualizar

O UpdatePostcomponente

Este componente é responsável por atualizar o título e o conteúdo de uma postagem existente.

@default.register
class PostUpdate(Component):
    title=public("")
    content=public("")


    def load(self, pk):
        self.post = Post.objects.filter(id=pk)[0]
        self.title=self.post.title
        self.content=self.post.content

    @public
    def update_post(self, title, content):
        self.post.title = title
        self.post.content = content

        self.post.save()

O loadmétodo recebe o ID do post que você deseja atualizar e o busca no banco de dados. Em seguida, ele atribui seu título e conteúdo aos atributos titlee contentpublic, respectivamente.

O update_postmétodo recebe o título e o conteúdo atualizados e os atribui à postagem buscada, depois os salva no banco de dados.

Abaixo está o template HTML do componente:

 template: django_html = """
        <div class="container">
            <h2>Update blog post</h2>
            <label> Title
            <em>*</em>
            </label>
            <input type="text" maxlength="255" x-model="title" name="title" required/>

            <label> Content
            <em>*</em>
            </label>
            <textarea rows="20" cols="80" x-model="content" name="content" required> </textarea>

            <button type="submit" @click="update_post(title, content)"><em>Submit</em></button>
        </div>
        """

O modelo acima renderiza o valor dos atributos public title e content por meio da x-modelpropriedade Alpine.js, enquanto o botão usa a @clickfunção Alpine.js para invocar o update_postmétodo e passar o novo valor do título e do conteúdo como argumentos.

No PostDetailmodelo criado na seção anterior, atualize o código HTML incluindo uma anchortag que direcionará o usuário para a página de pós-atualização na tela inicial.

@default.register
class PostDetail(Component):
 
   ...

    template: django_html = """
        <article  {% ... attrs %} > 
            ...
            <a class="nav-item nav-link" href="{% url 'update-post' pk=post.id %}"><button id="update-button"> <em>Update</em> </button></a>
         ...
           
        </article>
    """

Em seguida, dentro da pasta template, crie um post_update.htmlarquivo que servirá como template HTML raiz para o PostUpdatecomponente. Adicione o seguinte trecho ao arquivo:

{% load tetra %}
<!Doctype html>
<html>
  <head>
    <title> Update post </title>
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
     <form enctype="multipart/form-data" method="POST">
      {% csrf_token %}
      {% @ post_update pk=pk / %}
    </form>
  </body>
  </html>

Por fim, atualize os arquivos views.pye urls.pycom o seguinte código, respectivamente:

def update_post(request, **kwargs):
    return render(request, 'post_update.html', kwargs)urlpatterns = [
     ...
     path("<int:pk>", views.update_post, name='update-post'),
     ...

]

Você pode navegar até a update-postpágina clicando no botão Atualizar na tela de detalhes da postagem.

Atualizar a tela de postagem do blog com caixas de título e conteúdo

O título da postagem do blog agora mostra "-atualizado"

Notas sobre a prontidão de produção da Tetra

No momento da redação deste artigo, o Tetra ainda está em seus estágios iniciais de desenvolvimento e atualmente oferece suporte ao Python 3.9 e superior. No entanto, a equipe da Tetra está trabalhando na expansão das funcionalidades desse framework para versões mais antigas do Python.

Uma coisa que você deve saber antes de iniciar a produção com o Tetra é que a documentação do framework precisa de muitas melhorias. Era muito conciso, pois algumas dependências não foram explicadas ou não foram detalhadas o suficiente. Por exemplo, a documentação não discute como lidar com imagens, e é por isso que criamos um aplicativo de blog para esta demonstração.

Foi só depois de concluir o projeto que percebi que a estrutura não é tão complexa quanto a documentação apresentada.

Conclusão

Este artigo apresentou o Tetra e seus componentes. Você aprendeu como o Tetra funciona e executa operações de pilha completa a partir de um único arquivo criando um aplicativo de blog simples que executa operações CRUD.

A página inicial do Tetra contém alguns exemplos adicionais de como você pode criar alguns aplicativos simples com o Tetra. Se você também estiver interessado em saber mais sobre esse framework, a documentação está disponível para orientá-lo. Você pode conferir a implementação completa do aplicativo de blog no GitHub .

Fonte: https://blog.logrocket.com/build-full-stack-app-tetra/

  #tetra  #fullstack #python 

Crie Um Aplicativo Full-stack Com Tetra

Cree Una Aplicación Completa Con Tetra

La mayoría de las aplicaciones de pila completa separan el código de frontend y backend en archivos distintos; la mayoría de los marcos web se construyen en base a esta estructura. A medida que aumenta la cantidad de archivos y líneas de código, puede aumentar la complejidad de su base de código, lo que dificulta aún más la depuración. La complejidad causada por estos archivos separados se minimizó mediante la introducción de un marco llamado Tetra .

Este tutorial le presentará el marco Tetra y sus componentes. También aprenderá a crear una aplicación de blog simple de pila completa que realiza funcionalidades CRUD utilizando Tetra.

¿Qué es Tetra?

Tetra es un marco de trabajo de pila completa creado con Django en el lado del servidor y Alpine.js para realizar la lógica de interfaz. Tetra le permite tener lógica de front-end y back-end en una ubicación unificada y reduce la complejidad del código en su aplicación. Conecta la implementación del backend con el frontend usando una clase conocida como clase Componente.

El componente Tetra

Un componente tetra es una unidad de código que maneja su lógica de Python, HTML, CSS y JavaScript como una entidad en un solo archivo de Python. Si está familiarizado con el marco de React , puede comparar el comportamiento de sus componentes con los componentes de Tetra, excepto que los componentes de React solo realizan las funciones de interfaz.

Los componentes pueden ser dependientes o independientes entre sí. Esto implica que puede invocar un componente desde otro o tenerlo como un componente independiente. Puede leer más información sobre el componente tetra aquí .

Construyamos una aplicación de blog Tetra

El resto de este tutorial lo guiará a través de cómo instalar Tetra en su aplicación Django y un flujo paso a paso de cómo crearía una aplicación de blog usando Tetra. La aplicación de blog se presentará desde una perspectiva de administrador, donde podrá crear una nueva publicación, actualizar una publicación existente, eliminar una publicación y ver todas las publicaciones de blog.

La aplicación no incluirá ninguna capa de autenticación o autorización. El objetivo es mantenerlo lo más simple posible mientras se enfoca en las funcionalidades principales de Tetra.

requisitos previos

  • Competencia en la creación de aplicaciones monolíticas utilizando Django .
  • Conocimiento práctico de HTML, CSS y JavaScript
  • Cualquier IDE o editor de texto adecuado
  • Python versión 3.9 o superior instalada en su máquina
  • administrador de paquetes npm instalado en su máquina

configuración del proyecto

El primer paso es crear un entorno virtual para la aplicación. Ejecute el siguiente comando en su terminal para configurar el directorio de su proyecto y el entorno virtual:

mkdir tetra
cd tetra
python -m venv tetra 
cd tetra
Scripts/activate

El siguiente paso es instalar Django. Dado que Tetra opera en el marco Django, es necesario integrar Django en su aplicación.

pip install django
django-admin startproject tetra_blog
cd tetra_blog

A continuación, cree la aplicación de blog:

python manage.py startapp blog

Agregue la aplicación de blog a la INSTALLED_APPSlista en el settings.pyarchivo, como se muestra a continuación:

INSTALLED_APPS = [
    'blog.apps.BlogConfig',
    ...
 ]

Dentro del directorio de la aplicación, cree un components.pyarchivo que contendrá todos los componentes que construirá en el proyecto.

instalación y configuración tetra

Después de configurar con éxito el proyecto Django, el siguiente paso es instalar el marco Tetra en su aplicación.

pip install tetraframework

En el settings.pyarchivo, agregue tetraa la INSTALLED_APPSlista, como se muestra a continuación:

INSTALLED_APPS = [
    ...
    'tetra',
    'django.contrib.staticfiles',
    ...
]

Asegúrese tetrade que aparece antes del django.contrib.staticfileselemento.

A continuación, querrá incluir tetra.middleware.TetraMiddlewareal final de la MIDDLEWARElista. Esto agrega JavaScript y CSS de su componente a la plantilla HTML.

MIDDLEWARE = [ 
    ...
    'tetra.middleware.TetraMiddleware'
]

Aplique las siguientes modificaciones al urls.pyarchivo raíz para exponer los puntos finales de Tetra a través de sus métodos públicos:

from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    ...
 
    path('tetra/', include('tetra.urls')),
  ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

Instalar esbuild

Tetra construye sus componentes JS/CSS y los empaqueta usando esbuild . Esto le permite rastrear cualquier error que pueda ocurrir en la implementación de la interfaz hasta sus archivos fuente de Python.

npm init
npm install esbuild

Si está utilizando el sistema operativo Windows, deberá declarar explícitamente la ruta de compilación esbuilden su settings.pyarchivo:

TETRA_ESBUILD_PATH = '<absolute-path-to-project-root-directory>/node_modules/.bin/esbuild.cmd'

Modelo de publicación de blog

La aplicación realizará funciones CRUD en una publicación de blog. El Postmodelo constará de tres atributos: título, contenido y fecha.

Agregue el siguiente código al models.pyarchivo para configurar el Postmodelo:

from django.db import models
from django.utils import timezone
from django.urls import reverse


class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    date_posted = models.DateTimeField(default=timezone.now)


    def __str__(self):
        return self.title
    // generate a reverse url for the model
    def get_absolute_url(self):
        return reverse('post-detail', kwargs={'pk': self.id})

Ejecute los comandos de migración para crear una tabla para el modelo:

python manage.py makemigrations
python manage.py migrate

el AddPostcomponente

Este componente es responsable de representar la interfaz de usuario para crear una nueva publicación. También contendrá la lógica de Python que necesitamos para crear un Postmodelo y conservar los datos en la base de datos.

Cree add_post.pyun archivo en la componentscarpeta y agregue el siguiente código al archivo:

from sourcetypes import javascript, css, django_html
from tetra import Component, public, Library
from ..models import Post

default = Library()

@default.register
class AddPost(Component):
    title=public("")
    content=public("")

 
    def load(self):
        self.post = Post.objects.filter(id=0)


    @public
    def add_post(self, title, content):
        post = Post(
            title = title,
            content = content
        )
        post.save()

En el código anterior, la AddPostclase es una subclase de la clase de componente Tetra, que es la clase base sobre la que construye sus componentes personalizados. Usando el @default.registerdecorador, registra su AddPostcomponente en la biblioteca de Tetra.

Las variables titley son atributos públicos del componente, cada uno con un valor inicial de una cadena vacía. Los valores de están disponibles para las plantillas, JavaScript y la lógica del servidor.contentpublic attributes

El loadmétodo se ejecuta cuando se inicia el componente y cuando se reanuda desde un estado guardado. Puede pensar en el loadmétodo como el constructor del componente; se ejecuta cuando invoca el componente desde una plantilla.

El add_postmétodo es un método público que recibe el título y el contenido como argumentos para crear una Postinstancia y luego lo guarda en la base de datos. Al igual que los atributos públicos, los métodos públicos están expuestos a la plantilla, JavaScript y Python. Declaras un método como público agregando el @publicdecorador encima de la firma del método.

Este es el código HTML que debe incluir en el add_post.pyarchivo como parte del AddPostcomponente:

template: django_html = """
   
    <div class="container">
        <h2>Add blog post</h2>
        <label> Title
        <em>*</em>
        </label>
        <input type="text" maxlength="255" x-model="title" name="title" placeholder="Input post title" required/>

        <label> Content
        <em>*</em>
        </label>
        <textarea rows="20" cols="80" x-model="content" name="content" placeholder="Input blog content" required /> </textarea>

        <button type="submit" @click="addPost(title, content)"><em>Submit</em></button>
    </div>
    
    """

El campo de entrada recibe el título de la publicación y lo vincula al titleatributo público a través de la propiedad x-model de Alpine.js . Del mismo modo, textarearecibe el contenido de la publicación del blog y vincula el valor al contentatributo público del componente.

Usando la directiva Alpine.js @clickdentro de la etiqueta del botón, la plantilla invoca el addPostmétodo JavaScript:

script: javascript = """
    export default {

        addPost(title, content){
            this.add_post(title, content)   
        }
        
    }
    """

El método JavaScript addPostpasa los valores obtenidos del título y contenido como argumentos al add_postmétodo público del componente. También puede invocar el add_postmétodo público directamente desde la plantilla HTML anterior.

El objetivo de pasarlo a través de la función de JavaScript aquí es demostrar cómo realizaría una operación de JavaScript dentro de su componente Tetra. Esto es útil para situaciones en las que desea tener más control sobre el comportamiento del usuario, como potencialmente deshabilitar un botón después de que un usuario haya hecho clic en él para evitar que envíe varias solicitudes mientras procesa las anteriores.

Aquí está el código CSS para diseñar la plantilla:

style: css = """
    .container {
        display: flex;
        flex-direction: column;
        align-items: left;
        justify-content: center;
        border-style: solid;
        width: fit-content;
        margin: auto;
        margin-top: 50px;
        width: 50%;
        border-radius: 15px;
        padding: 30px;
    }

    input, textarea, label{
        margin-bottom: 30px;
        margin-left: 20px;
        ;
    }

    label {
        font-weight: bold;
    }

    input{
        height: 40px;
    }

    h2 {
        text-align: center;
    }

    button {
        width: 150px;
        padding: 10px;
        border-radius: 9px;
        border-style: none;
        background: green;
        color: white;
        margin: auto;
    }
    
    """

El siguiente paso es invocar el AddPostcomponente desde la plantilla de vista de Django. Cree un add_post.htmlarchivo en la carpeta de la aplicación de blog templatesque creó en la sección anterior de este tutorial. Agregue el siguiente fragmento de código al archivo:

{% load tetra %}
<!Doctype html>
<html lang="en">
  <head>
    <title> Add post </title>
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
     <form enctype="multipart/form-data" method="POST">
      {% csrf_token %}
      {% @ add_post / %}
    </form>
  </body>
  </html>

Esta plantilla comienza cargando las etiquetas de la plantilla Tetra en la plantilla. Logra esto a través del {% load tetra %}comando representado en la parte superior del código. También deberá inyectar CSS y JavaScript en la plantilla a través de {% tetra_styles %}y {% tetra_scripts}, respectivamente.

De forma predeterminada, Tetra no incluye Alpine.js en su plantilla. Debe declararlo explícitamente agregando include_alpine=Trueal inyectar el JavaScript del componente.

La {% @ add_post / %}etiqueta dentro de la formetiqueta invoca el loadmétodo del AddPostcomponente y representa el contenido HTML que declaró anteriormente al crear el componente.

Tenga en cuenta que el nombre del componente utilizado para cargar el componente está en mayúsculas y minúsculas. Esta es la configuración predeterminada para invocar componentes desde plantillas. También puede establecer un nombre personalizado cuando crea el componente, como se muestra a continuación:

...
@default.register(name="custom_component_name")
class AddPost(Component):
...

Luego puede cargar el componente usando {% @ custom_component_name / %}.

A continuación, agregue el siguiente fragmento de código al views.pyarchivo:

from django.shortcuts import render

def add_post(request):
    return render(request, 'add_post.html')

Cree un urls.pyarchivo en el directorio de la aplicación de blog y agregue el siguiente fragmento de código al archivo:

from django.urls import path
from . import views


urlpatterns = [
     path("add", views.add_post, name='add-post'),

]

urls.pyEn el archivo raíz , agregue la siguiente ruta:

urlpatterns = [
    ...
    path('tetra/', include('tetra.urls')),
    path('post/', include('blog.urls'))
]

Ejecute la aplicación con python manage.py runserver command. Vea la página en su navegador a través de localhost:8000/post/add.

Aquí está la salida de la página:

Agregar página de publicación de blog con cuadro de título y cuadro de contenido

el PostItemcomponente

El PostItemcomponente contiene la plantilla para representar una publicación creada en la pantalla de inicio.

@default.register
class PostItem(Component):
    
    def load(self, post):
        self.post = post

El loadmétodo recibe la Postinstancia como argumento y la expone a la plantilla HTML que representa su título y contenido en la pantalla.

 template: django_html = """
   
    <article class="post-container" {% ... attrs %}>
            <small class="article-metadata">{{ post.date_posted.date}}</small>
            <p class="article-title"> {{ post.title }}</p>
            <p class="article-content">{{ post.content }}</p>

        </article>
            
    """

La {% ... attrs %}etiqueta es una etiqueta de atributo de Tetra que la plantilla utiliza para recibir los argumentos que se le pasan al invocar el componente. Al recibir argumentos usando la etiqueta de atributos, debe declarar la etiqueta en el nodo raíz de la plantilla HTML, como se hizo en la etiqueta del artículo en el fragmento anterior.

Aquí está la implementación CSS de la plantilla:

style: css = """
    
    .article-metadata {
        padding-bottom: 1px;
        margin-bottom: 4px;
        border-bottom: 1px solid #e3e3e3;
        
    }


    .article-title{
        font-size: x-large;
        font-weight: 700;
    }

    .article-content {
        white-space: pre-line;
    }

    .post-container{
        margin: 50px;
    }

    a.article-title:hover {
        color: #428bca;
        text-decoration: none;
    }

    .article-content {
        white-space: pre-line;
    }

    a.nav-item{
        text-align: right;
        margin-right: 100px;
    }

    h1 {
       text-align: center;
    }
    """

Así es como se verá una publicación a través del PostItemcomponente:

El componente PostItem muestra la publicación con el texto predeterminado

el ViewPostscomponente

Este componente es responsable de renderizar todas las publicaciones creadas. Agregue el siguiente fragmento de código al components.pyarchivo:

@default.register
class PostView(Component):

    def load(self):
        self.posts = Post.objects.all()

    template: django_html = """
        <div>
            <h1> Tetra blog </h1>
            <div class="navbar-nav">
                <a class="nav-item nav-link" href="{% url 'add-post' %}">New Post</a>
            <div>
            <div class="list-group">
                {% for post in posts %}
                    {% @ post_item post=post key=post.id / %}
                {% endfor %}
            </div>
         </div>
        """

El loadmétodo en los componentes recupera todas las publicaciones creadas de la base de datos. La plantilla HTML contiene una etiqueta de anclaje que dirige a la add-postURL para crear una nueva publicación.

Para cada publicación obtenida de la base de datos, el HTML crea un PostItemcomponente al pasar el objeto de la publicación como su argumento dentro del bucle for.

A continuación, invoque el ViewPostcomponente desde la plantilla de vista de Django. Cree un home.htmlarchivo en la carpeta de la aplicación de blog templatesy agregue el siguiente fragmento de código al archivo:

{% load tetra %}
<!Doctype html>
<html lang="en">
  <head>
    <title> Blog home </title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
      {% @ view_post / %}
  </body>
  </html>

A continuación, agregue lo siguiente al views.pyarchivo:

def home(request):
    return render(request, 'home.html')

Por último, actualice la lista en el archivo urlpatternsde la aplicación de blog .urls.py

urlpatterns = [
     path("", views.home, name='home'),
    ...
]

Puede ver la página a través de localhost:8000/post.

blog tetra

el PostDetailcomponente

Este componente representará la publicación completa en una sola página. La página también contendrá dos botones: uno para eliminar y actualizar la publicación. Agregue el siguiente código al components.pyarchivo:

@default.register
class PostDetail(Component):
 
    def load(self, pk):
        self.post = Post.objects.filter(id=pk)[0]

    @public(update=False)
    def delete_item(self):
        Post.objects.filter(id=self.post.id).delete()
        self.client._removeComponent()

El loadmétodo recibe el idde la publicación a través de la pkvariable y obtiene la Postinstancia, cuyo ID coincide con el pkvalor de la base de datos.

El delete_itemmétodo elimina la Postinstancia de la base de datos y la elimina automáticamente de la pantalla de inicio. De forma predeterminada, un método público volverá a representar un componente cuando lo invoque. Al configurar la updatepropiedad Falseen el @publicdecorador, se asegura de que no intente volver a procesar una publicación eliminada anteriormente.

Aquí está la plantilla HTML:

 template: django_html = """
        <article > 
            <small class="text-muted">{{ post.date_posted.date}}</small>
                
            <h2 class="article-title">{{ post.title }}</h2>
            <p class="article-content">{{ post.content }}</p>

            <div class="post-buttons">
            <button id="delete-button" type="submit" @click="delete_item()"><em>Delete</em></button>
           <button id="update-button"> <em>Update</em> </button>
            </div>
           
        </article>
    """

La plantilla recupera la fecha, el título y el contenido de la publicación obtenida del loadmétodo y representa estos valores. También contiene botones para eliminar y actualizar la publicación. El botón Eliminar invoca el delete_itemmétodo para realizar la operación de eliminación en la publicación. Implementaremos el botón Actualizar en la sección siguiente.

Aquí está el CSS para la plantilla:

 style: css = """

        article{
            margin: 100px;
        }

        .post-buttons{
            position: absolute;
            right: 0;
        }

        #delete-button, #update-button{
            width: 150px;
            padding: 10px;
            border-radius: 9px;
            border-style: none;
            font-weight: bold;
            margin: auto;
        }

        #update-button{
            background: blue;
            color: white;
        }

        #delete-button{
            background: red;
            color: white;
        }

    """

En la PostItemplantilla creada en la sección anterior, actualice el código HTML incluyendo una anchoretiqueta que dirigirá al usuario a la página de detalles de la publicación desde la pantalla de inicio.

@default.register
class PostItem(Component):
   
  ...        
   
    template: django_html = """
   
    <article class="post-container" >
           ...
            <a href="{% url 'post-detail' pk=post.id %}"> {{ post.title }}</a>
          ...

        </article>
            
    """

En la carpeta de plantillas, cree un post-detail.htmlarchivo que sirva como archivo HTML raíz para la página de detalles posteriores e incluya el siguiente código en el archivo:

A continuación, actualice los archivos views.pyy urls.pypara incluir la ruta a la página de detalles posteriores:

def post_detail(request, **kwargs):
    return render(request, 'post_detail.html', kwargs)urlpatterns = [
     path("<int:pk>/", views.post_detail, name='post-detail')
]

Vea los detalles de la publicación en su navegador haciendo clic en el título de la publicación desde la página de inicio del blog.

Publicar detalles con botones de eliminar y actualizar

el UpdatePostcomponente

Este componente es responsable de actualizar el título y el contenido de una publicación existente.

@default.register
class PostUpdate(Component):
    title=public("")
    content=public("")


    def load(self, pk):
        self.post = Post.objects.filter(id=pk)[0]
        self.title=self.post.title
        self.content=self.post.content

    @public
    def update_post(self, title, content):
        self.post.title = title
        self.post.content = content

        self.post.save()

El loadmétodo recibe el ID de la publicación que desea actualizar y lo obtiene de la base de datos. Luego, asigna su título y contenido a los atributos titley contentpúblico respectivamente.

El update_postmétodo recibe el título y el contenido actualizados y los asigna a la publicación obtenida, luego los guarda en la base de datos.

A continuación se muestra la plantilla HTML del componente:

 template: django_html = """
        <div class="container">
            <h2>Update blog post</h2>
            <label> Title
            <em>*</em>
            </label>
            <input type="text" maxlength="255" x-model="title" name="title" required/>

            <label> Content
            <em>*</em>
            </label>
            <textarea rows="20" cols="80" x-model="content" name="content" required> </textarea>

            <button type="submit" @click="update_post(title, content)"><em>Submit</em></button>
        </div>
        """

La plantilla anterior representa el valor de los atributos públicos del título y el contenido a través de la propiedad Alpine.js x-model, mientras que el botón usa la @clickfunción Alpine.js para invocar el update_postmétodo y pasar el nuevo valor del título y el contenido como argumentos.

En la PostDetailplantilla creada en la sección anterior, actualice el código HTML incluyendo una anchoretiqueta que dirigirá al usuario a la página de actualización de publicaciones desde la pantalla de inicio.

@default.register
class PostDetail(Component):
 
   ...

    template: django_html = """
        <article  {% ... attrs %} > 
            ...
            <a class="nav-item nav-link" href="{% url 'update-post' pk=post.id %}"><button id="update-button"> <em>Update</em> </button></a>
         ...
           
        </article>
    """

A continuación, dentro de la carpeta de plantillas, cree un post_update.htmlarchivo que sirva como plantilla HTML raíz para el PostUpdatecomponente. Agregue el siguiente fragmento de código al archivo:

{% load tetra %}
<!Doctype html>
<html>
  <head>
    <title> Update post </title>
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
     <form enctype="multipart/form-data" method="POST">
      {% csrf_token %}
      {% @ post_update pk=pk / %}
    </form>
  </body>
  </html>

Finalmente, actualice los archivos views.pyy urls.pycon el siguiente código respectivamente:

def update_post(request, **kwargs):
    return render(request, 'post_update.html', kwargs)urlpatterns = [
     ...
     path("<int:pk>", views.update_post, name='update-post'),
     ...

]

Puede navegar a la update-postpágina haciendo clic en el botón Actualizar en la pantalla de detalles de la publicación.

Actualizar la pantalla de publicación de blog con cuadros de título y contenido

El título de la publicación del blog ahora muestra "-actualizado"

Notas sobre la preparación para la producción de Tetra

Al momento de escribir este artículo, Tetra aún se encuentra en sus primeras etapas de desarrollo y actualmente es compatible con Python 3.9 y superior. Sin embargo, el equipo de Tetra está trabajando para expandir las funcionalidades de este marco a versiones anteriores de Python.

Una cosa que debe saber antes de comenzar la producción con Tetra es que la documentación del marco necesita muchas mejoras. Era demasiado conciso, ya que algunas dependencias no se explicaban en absoluto o no se detallaban lo suficiente. Por ejemplo, la documentación no explica cómo manejar las imágenes, razón por la cual creamos una aplicación de blog para esta demostración.

No fue hasta después de haber completado el proyecto que me di cuenta de que el marco no es tan complejo como lo presentaba la documentación.

Conclusión

Este artículo le presentó Tetra y sus componentes. Aprendió cómo funciona Tetra y realiza operaciones de pila completa desde un solo archivo mediante la creación de una aplicación de blog simple que realiza operaciones CRUD.

La página de inicio de Tetra contiene algunos ejemplos adicionales de cómo puede crear algunas aplicaciones simples con Tetra. Si también está interesado en obtener más información sobre este marco, la documentación está disponible para guiarlo. Puede consultar la implementación completa de la aplicación de blog en GitHub .

Fuente: https://blog.logrocket.com/build-full-stack-app-tetra/

  #tetra  #fullstack #python 

Cree Una Aplicación Completa Con Tetra
Léon  Peltier

Léon Peltier

1660338000

Créez Une Application Complète Avec Tetra

La plupart des applications full-stack séparent le code frontend et backend dans des fichiers distincts ; la plupart des frameworks Web sont construits sur la base de cette structure. À mesure que le nombre de fichiers et de lignes de code augmente, cela peut augmenter la complexité de votre base de code, ce qui la rend encore plus difficile à déboguer. La complexité causée par ces fichiers séparés a été minimisée grâce à l'introduction d'un framework appelé Tetra .

Ce tutoriel vous présentera le framework Tetra et ses composants. Vous apprendrez également à créer une simple application de blog complète qui exécute les fonctionnalités CRUD à l'aide de Tetra.

Qu'est-ce que Tétra ?

Tetra est un framework full-stack construit avec Django côté serveur et Alpine.js pour exécuter la logique frontale. Tetra vous permet d'avoir une logique frontale et dorsale dans un emplacement unifié et réduit la complexité du code dans votre application. Il connecte l'implémentation backend avec le frontend à l'aide d'une classe connue sous le nom de classe Component.

Le composant Tetra

Un composant tétra est une unité de code qui gère sa logique Python, HTML, CSS et JavaScript comme une entité dans un seul fichier Python. Si vous connaissez le framework React , vous pouvez comparer le comportement de ses composants aux composants Tetra, sauf que les composants React n'exécutent que les fonctionnalités frontales.

Les composants peuvent être dépendants ou indépendants les uns des autres. Cela implique que vous pouvez invoquer un composant à partir d'un autre ou l'avoir en tant que composant autonome. Vous pouvez lire plus d'informations sur le composant tétra ici .

Créons une application de blog Tetra

Le reste de ce didacticiel vous guidera dans l'installation de Tetra dans votre application Django et vous expliquera étape par étape comment créer une application de blog à l'aide de Tetra. L'application de blog sera présentée du point de vue de l'administrateur, où vous pourrez créer un nouveau message, mettre à jour un message existant, supprimer un message et afficher tous les messages du blog.

L'application n'inclura aucune couche d'authentification ou d'autorisation. L'objectif est de le garder aussi simple que possible tout en se concentrant sur les fonctionnalités de base de Tetra.

Conditions préalables

  • Maîtrise de la création d'applications monolithiques à l'aide de Django
  • Connaissance pratique de HTML, CSS et JavaScript
  • Tout IDE ou éditeur de texte approprié
  • Python version 3.9 ou supérieure installé sur votre machine
  • gestionnaire de paquets npm installé sur votre machine

Configuration du projet

La première étape consiste à créer un environnement virtuel pour l'application. Exécutez la commande suivante dans votre terminal pour configurer votre répertoire de projet et votre environnement virtuel :

mkdir tetra
cd tetra
python -m venv tetra 
cd tetra
Scripts/activate

L'étape suivante consiste à installer Django. Étant donné que Tetra fonctionne sur le framework Django, il est nécessaire d'intégrer Django dans votre application.

pip install django
django-admin startproject tetra_blog
cd tetra_blog

Ensuite, créez l'application de blog :

python manage.py startapp blog

Ajoutez l'application de blog à la INSTALLED_APPSliste du settings.pyfichier, comme indiqué ci-dessous :

INSTALLED_APPS = [
    'blog.apps.BlogConfig',
    ...
 ]

Dans le répertoire de l'application, créez un components.pyfichier qui contiendra tous les composants que vous construirez dans le projet.

Installation et configuration de Tetra

Après avoir configuré avec succès le projet Django, l'étape suivante consiste à installer le framework Tetra dans votre application.

pip install tetraframework

Dans le settings.pyfichier, ajoutez tetraà la INSTALLED_APPSliste, comme indiqué ci-dessous :

INSTALLED_APPS = [
    ...
    'tetra',
    'django.contrib.staticfiles',
    ...
]

Assurez -vous tetraest répertorié avant l' django.contrib.staticfilesélément.

Ensuite, vous voudrez inclure tetra.middleware.TetraMiddlewareà la fin de la MIDDLEWAREliste. Cela ajoute le JavaScript et le CSS de votre composant au modèle HTML.

MIDDLEWARE = [ 
    ...
    'tetra.middleware.TetraMiddleware'
]

Appliquez les modifications ci-dessous au urls.pyfichier racine pour exposer les points de terminaison de Tetra via vos méthodes publiques :

from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    ...
 
    path('tetra/', include('tetra.urls')),
  ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

Installer esbuild

Tetra construit vos composants JS/CSS et les empaquete à l'aide de esbuild . Cela vous permet de tracer toutes les erreurs pouvant survenir lors de l'implémentation de l'interface dans vos fichiers Python source.

npm init
npm install esbuild

Si vous utilisez le système d'exploitation Windows, vous devrez déclarer explicitement le chemin de construction pour esbuilddans votre settings.pyfichier :

TETRA_ESBUILD_PATH = '<absolute-path-to-project-root-directory>/node_modules/.bin/esbuild.cmd'

Modèle d'article de blog

L'application exécutera les fonctions CRUD sur un article de blog. Le Postmodèle comprendra trois attributs : titre, contenu et date.

Ajoutez le code suivant au models.pyfichier pour configurer le Postmodèle :

from django.db import models
from django.utils import timezone
from django.urls import reverse


class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    date_posted = models.DateTimeField(default=timezone.now)


    def __str__(self):
        return self.title
    // generate a reverse url for the model
    def get_absolute_url(self):
        return reverse('post-detail', kwargs={'pk': self.id})

Exécutez les commandes de migration pour créer une table pour le modèle :

python manage.py makemigrations
python manage.py migrate

Le AddPostcomposant

Ce composant est responsable du rendu de l'interface utilisateur pour créer un nouveau message. Il contiendra également la logique Python dont nous avons besoin pour créer un Postmodèle et conserver les données dans la base de données.

Créez add_post.pyun fichier dans le componentsdossier et ajoutez le code suivant au fichier :

from sourcetypes import javascript, css, django_html
from tetra import Component, public, Library
from ..models import Post

default = Library()

@default.register
class AddPost(Component):
    title=public("")
    content=public("")

 
    def load(self):
        self.post = Post.objects.filter(id=0)


    @public
    def add_post(self, title, content):
        post = Post(
            title = title,
            content = content
        )
        post.save()

Dans le code ci-dessus, la AddPostclasse est une sous-classe de la classe de composants Tetra, qui est la classe de base sur laquelle vous construisez vos composants personnalisés. À l'aide du @default.registerdécorateur, vous enregistrez votre AddPostcomposant dans la bibliothèque Tetra.

Les variables titleet sont des attributs publics du composant, chacun avec une valeur initiale d'une chaîne vide. Les valeurs de sont disponibles pour les modèles, JavaScript et la logique du serveur.contentpublic attributes

La loadméthode s'exécute lorsque le composant démarre et lorsqu'il reprend à partir d'un état enregistré. Vous pouvez considérer la loadméthode comme le constructeur du composant ; il s'exécute lorsque vous appelez le composant à partir d'un modèle.

La add_postméthode est une méthode publique qui reçoit le titre et le contenu comme arguments pour créer une Postinstance, puis l'enregistre dans la base de données. Tout comme les attributs publics, les méthodes publiques sont exposées au modèle, JavaScript et Python. Vous déclarez une méthode comme publique en ajoutant le @publicdécorateur au-dessus de la signature de la méthode.

Voici le code HTML que vous devez inclure dans le add_post.pyfichier dans le cadre du AddPostcomposant :

template: django_html = """
   
    <div class="container">
        <h2>Add blog post</h2>
        <label> Title
        <em>*</em>
        </label>
        <input type="text" maxlength="255" x-model="title" name="title" placeholder="Input post title" required/>

        <label> Content
        <em>*</em>
        </label>
        <textarea rows="20" cols="80" x-model="content" name="content" placeholder="Input blog content" required /> </textarea>

        <button type="submit" @click="addPost(title, content)"><em>Submit</em></button>
    </div>
    
    """

Le champ de saisie reçoit le titre du message et le lie à l' titleattribut public via la propriété x-model Alpine.js . De même, le textareareçoit le contenu de l'article de blog et lie la valeur à l' contentattribut public du composant.

À l'aide de la directive Alpine.js @clickdans la balise du bouton, le modèle invoque la addPostméthode JavaScript :

script: javascript = """
    export default {

        addPost(title, content){
            this.add_post(title, content)   
        }
        
    }
    """

La méthode JavaScript addPosttransmet les valeurs obtenues à partir du titre et du contenu en tant qu'arguments à la add_postméthode publique du composant. Vous pouvez également invoquer la add_postméthode publique directement à partir du modèle HTML ci-dessus.

Le but de le faire passer par la fonction JavaScript ici est de montrer comment vous effectueriez une opération JavaScript dans votre composant Tetra. Ceci est utile dans les situations où vous souhaitez avoir plus de contrôle sur le comportement de l'utilisateur, comme la désactivation potentielle d'un bouton après qu'un utilisateur a cliqué dessus pour l'empêcher d'envoyer plusieurs demandes lors du traitement des précédentes.

Voici le code CSS pour styliser le modèle :

style: css = """
    .container {
        display: flex;
        flex-direction: column;
        align-items: left;
        justify-content: center;
        border-style: solid;
        width: fit-content;
        margin: auto;
        margin-top: 50px;
        width: 50%;
        border-radius: 15px;
        padding: 30px;
    }

    input, textarea, label{
        margin-bottom: 30px;
        margin-left: 20px;
        ;
    }

    label {
        font-weight: bold;
    }

    input{
        height: 40px;
    }

    h2 {
        text-align: center;
    }

    button {
        width: 150px;
        padding: 10px;
        border-radius: 9px;
        border-style: none;
        background: green;
        color: white;
        margin: auto;
    }
    
    """

L'étape suivante consiste à invoquer le AddPostcomposant à partir du modèle de vue Django. Créez un add_post.htmlfichier dans le dossier de l'application de blog templatesque vous avez créé dans la section précédente de ce didacticiel. Ajoutez l'extrait de code suivant au fichier :

{% load tetra %}
<!Doctype html>
<html lang="en">
  <head>
    <title> Add post </title>
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
     <form enctype="multipart/form-data" method="POST">
      {% csrf_token %}
      {% @ add_post / %}
    </form>
  </body>
  </html>

Ce modèle commence par charger les balises du modèle Tetra dans le modèle. Il y parvient grâce à la {% load tetra %}commande décrite en haut du code. Vous devrez également injecter le CSS et le JavaScript dans le modèle via {% tetra_styles %}et {% tetra_scripts}, respectivement.

Par défaut, Tetra n'inclut pas Alpine.js dans votre modèle. Vous devez le déclarer explicitement en l'ajoutant include_alpine=Truelors de l'injection du JavaScript du composant.

La {% @ add_post / %}balise à l'intérieur de la formbalise invoque la loadméthode du AddPostcomposant et restitue le contenu HTML que vous avez déclaré ci-dessus lors de la création du composant.

Notez que le nom du composant utilisé pour charger le composant est en casse serpent. Il s'agit de la configuration par défaut pour appeler des composants à partir de modèles. Vous pouvez également définir un nom personnalisé lorsque vous créez le composant, comme illustré ci-dessous :

...
@default.register(name="custom_component_name")
class AddPost(Component):
...

Ensuite, vous pouvez charger le composant à l'aide de {% @ custom_component_name / %}.

Ensuite, ajoutez l'extrait ci-dessous au views.pyfichier :

from django.shortcuts import render

def add_post(request):
    return render(request, 'add_post.html')

Créez un urls.pyfichier dans le répertoire de l'application de blog et ajoutez-y l'extrait de code suivant :

from django.urls import path
from . import views


urlpatterns = [
     path("add", views.add_post, name='add-post'),

]

Dans le fichier racine urls.py, ajoutez le chemin ci-dessous :

urlpatterns = [
    ...
    path('tetra/', include('tetra.urls')),
    path('post/', include('blog.urls'))
]

Exécutez l'application avec python manage.py runserver command. Affichez la page sur votre navigateur via localhost:8000/post/add.

Voici la sortie de la page :

Ajouter une page d'article de blog avec une zone de titre et une zone de contenu

Le PostItemcomposant

Le PostItemcomposant contient le modèle de rendu d'une publication créée sur l'écran d'accueil.

@default.register
class PostItem(Component):
    
    def load(self, post):
        self.post = post

La loadméthode reçoit l' Postinstance comme argument et l'expose au modèle HTML qui affiche son titre et son contenu à l'écran.

 template: django_html = """
   
    <article class="post-container" {% ... attrs %}>
            <small class="article-metadata">{{ post.date_posted.date}}</small>
            <p class="article-title"> {{ post.title }}</p>
            <p class="article-content">{{ post.content }}</p>

        </article>
            
    """

La {% ... attrs %}balise est une balise d'attribut Tetra que le modèle utilise pour recevoir les arguments qui lui sont transmis lors de l'appel du composant. Lorsque vous recevez des arguments à l'aide de la balise attributs, vous devez déclarer la balise dans le nœud racine du modèle HTML, comme cela est fait dans la balise article de l'extrait ci-dessus.

Voici l'implémentation CSS du modèle :

style: css = """
    
    .article-metadata {
        padding-bottom: 1px;
        margin-bottom: 4px;
        border-bottom: 1px solid #e3e3e3;
        
    }


    .article-title{
        font-size: x-large;
        font-weight: 700;
    }

    .article-content {
        white-space: pre-line;
    }

    .post-container{
        margin: 50px;
    }

    a.article-title:hover {
        color: #428bca;
        text-decoration: none;
    }

    .article-content {
        white-space: pre-line;
    }

    a.nav-item{
        text-align: right;
        margin-right: 100px;
    }

    h1 {
       text-align: center;
    }
    """

Voici à quoi ressemblera une publication à travers le PostItemcomposant :

Le composant PostItem affiche la publication avec le texte par défaut

Le ViewPostscomposant

Ce composant est responsable du rendu de tous les messages créés. Ajoutez l'extrait de code suivant au components.pyfichier :

@default.register
class PostView(Component):

    def load(self):
        self.posts = Post.objects.all()

    template: django_html = """
        <div>
            <h1> Tetra blog </h1>
            <div class="navbar-nav">
                <a class="nav-item nav-link" href="{% url 'add-post' %}">New Post</a>
            <div>
            <div class="list-group">
                {% for post in posts %}
                    {% @ post_item post=post key=post.id / %}
                {% endfor %}
            </div>
         </div>
        """

La loadméthode dans les composants récupère tous les messages créés à partir de la base de données. Le modèle HTML contient une balise d'ancrage qui dirige vers l' add-postURL pour créer une nouvelle publication.

Pour chaque publication extraite de la base de données, le code HTML crée un PostItemcomposant en passant l'objet de publication comme argument dans la boucle for.

Ensuite, appelez le ViewPostcomposant à partir du modèle de vue Django. Créez un home.htmlfichier dans le dossier de l'application de blog templateset ajoutez-y l'extrait de code suivant :

{% load tetra %}
<!Doctype html>
<html lang="en">
  <head>
    <title> Blog home </title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
      {% @ view_post / %}
  </body>
  </html>

Ensuite, ajoutez ce qui suit au views.pyfichier :

def home(request):
    return render(request, 'home.html')

Enfin, mettez à jour la urlpatternsliste dans le fichier de l'application de blog urls.py.

urlpatterns = [
     path("", views.home, name='home'),
    ...
]

Vous pouvez afficher la page via localhost:8000/post.

Blog tétra

Le PostDetailcomposant

Ce composant affichera le message complet sur une seule page. La page contiendra également deux boutons : un pour supprimer et mettre à jour le message. Ajoutez le code suivant au components.pyfichier :

@default.register
class PostDetail(Component):
 
    def load(self, pk):
        self.post = Post.objects.filter(id=pk)[0]

    @public(update=False)
    def delete_item(self):
        Post.objects.filter(id=self.post.id).delete()
        self.client._removeComponent()

La loadméthode reçoit le idpost via la pkvariable et récupère l' Postinstance, dont l'ID correspond à la pkvaleur de la base de données.

La delete_itemméthode supprime l' Postinstance de la base de données, la supprimant automatiquement de l'écran d'accueil. Par défaut, une méthode publique restituera un composant lorsque vous l'invoquerez. En définissant la updatepropriété sur Falsedans le @publicdécorateur, il garantit qu'il n'essaie pas de restituer un message précédemment supprimé.

Voici le modèle HTML :

 template: django_html = """
        <article > 
            <small class="text-muted">{{ post.date_posted.date}}</small>
                
            <h2 class="article-title">{{ post.title }}</h2>
            <p class="article-content">{{ post.content }}</p>

            <div class="post-buttons">
            <button id="delete-button" type="submit" @click="delete_item()"><em>Delete</em></button>
           <button id="update-button"> <em>Update</em> </button>
            </div>
           
        </article>
    """

Le modèle récupère la date, le titre et le contenu de la publication extraite de la loadméthode et restitue ces valeurs. Il contient également des boutons pour supprimer et mettre à jour la publication. Le bouton Supprimerdelete_item invoque la méthode pour effectuer l'opération de suppression sur la publication. Nous implémenterons le bouton Mettre à jour dans la section suivante.

Voici le CSS du modèle :

 style: css = """

        article{
            margin: 100px;
        }

        .post-buttons{
            position: absolute;
            right: 0;
        }

        #delete-button, #update-button{
            width: 150px;
            padding: 10px;
            border-radius: 9px;
            border-style: none;
            font-weight: bold;
            margin: auto;
        }

        #update-button{
            background: blue;
            color: white;
        }

        #delete-button{
            background: red;
            color: white;
        }

    """

Dans le PostItemmodèle créé dans la section précédente, mettez à jour le code HTML en incluant une anchorbalise qui dirigera l'utilisateur vers la page de détail de la publication à partir de l'écran d'accueil.

@default.register
class PostItem(Component):
   
  ...        
   
    template: django_html = """
   
    <article class="post-container" >
           ...
            <a href="{% url 'post-detail' pk=post.id %}"> {{ post.title }}</a>
          ...

        </article>
            
    """

Dans le dossier des modèles, créez un post-detail.htmlfichier qui servira de fichier HTML racine pour la page de post-détail et incluez le code suivant dans le fichier :

Ensuite, mettez à jour les fichiers views.pyet urls.pypour inclure le chemin d'accès à la page post-détail :

def post_detail(request, **kwargs):
    return render(request, 'post_detail.html', kwargs)urlpatterns = [
     path("<int:pk>/", views.post_detail, name='post-detail')
]

Affichez les détails de la publication dans votre navigateur en cliquant sur le titre de la publication depuis la page d'accueil du blog.

Afficher les détails avec les boutons de suppression et de mise à jour

Le UpdatePostcomposant

Ce composant est responsable de la mise à jour du titre et du contenu d'un article existant.

@default.register
class PostUpdate(Component):
    title=public("")
    content=public("")


    def load(self, pk):
        self.post = Post.objects.filter(id=pk)[0]
        self.title=self.post.title
        self.content=self.post.content

    @public
    def update_post(self, title, content):
        self.post.title = title
        self.post.content = content

        self.post.save()

La loadméthode reçoit l'ID du message que vous souhaitez mettre à jour et le récupère dans la base de données. Ensuite, il attribue son titre et son contenu aux attributs titleet contentpublic respectivement.

La update_postméthode reçoit le titre et le contenu mis à jour et les attribue au message récupéré, puis l'enregistre dans la base de données.

Ci-dessous le modèle HTML du composant :

 template: django_html = """
        <div class="container">
            <h2>Update blog post</h2>
            <label> Title
            <em>*</em>
            </label>
            <input type="text" maxlength="255" x-model="title" name="title" required/>

            <label> Content
            <em>*</em>
            </label>
            <textarea rows="20" cols="80" x-model="content" name="content" required> </textarea>

            <button type="submit" @click="update_post(title, content)"><em>Submit</em></button>
        </div>
        """

Le modèle ci-dessus restitue la valeur des attributs publics de titre et de contenu via la x-modelpropriété Alpine.js, tandis que le bouton utilise la @clickfonction Alpine.js pour appeler la update_postméthode et transmettre la nouvelle valeur du titre et du contenu en tant qu'arguments.

Dans le PostDetailmodèle créé dans la section précédente, mettez à jour le code HTML en incluant une anchorbalise qui dirigera l'utilisateur vers la page de publication de mise à jour depuis l'écran d'accueil.

@default.register
class PostDetail(Component):
 
   ...

    template: django_html = """
        <article  {% ... attrs %} > 
            ...
            <a class="nav-item nav-link" href="{% url 'update-post' pk=post.id %}"><button id="update-button"> <em>Update</em> </button></a>
         ...
           
        </article>
    """

Ensuite, dans le dossier du modèle, créez un post_update.htmlfichier qui servira de modèle HTML racine pour le PostUpdatecomposant. Ajoutez l'extrait de code suivant au fichier :

{% load tetra %}
<!Doctype html>
<html>
  <head>
    <title> Update post </title>
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
     <form enctype="multipart/form-data" method="POST">
      {% csrf_token %}
      {% @ post_update pk=pk / %}
    </form>
  </body>
  </html>

Enfin, mettez à jour les fichiers views.pyet urls.pyavec respectivement le code suivant :

def update_post(request, **kwargs):
    return render(request, 'post_update.html', kwargs)urlpatterns = [
     ...
     path("<int:pk>", views.update_post, name='update-post'),
     ...

]

Vous pouvez accéder à la update-postpage en cliquant sur le bouton Mettre à jour sur l'écran de détail de la publication.

Mettre à jour l'écran des articles de blog avec les zones de titre et de contenu

Le titre de l'article de blog affiche désormais "-updated"

Notes sur la préparation à la production de Tetra

Au moment de la rédaction de cet article, Tetra en est encore à ses premiers stades de développement et prend actuellement en charge Python 3.9 et supérieur. Cependant, l'équipe Tetra travaille à étendre les fonctionnalités de ce framework aux anciennes versions de Python.

Une chose que vous devez savoir avant de commencer la production avec Tetra est que la documentation du framework a besoin de beaucoup d'améliorations. C'était trop concis, car certaines dépendances n'étaient pas expliquées du tout ou n'étaient pas assez détaillées. Par exemple, la documentation ne traite pas de la gestion des images, c'est pourquoi nous avons créé une application de blog pour cette démo.

Ce n'est qu'après avoir terminé le projet que j'ai réalisé que le cadre n'était pas aussi complexe que la documentation le présentait.

Conclusion

Cet article vous a présenté Tetra et ses composants. Vous avez appris comment Tetra fonctionne et exécute des opérations complètes à partir d'un seul fichier en créant une application de blog simple qui effectue des opérations CRUD.

La page d'accueil de Tetra contient des exemples supplémentaires de la façon dont vous pouvez créer des applications simples avec Tetra. Si vous souhaitez également en savoir plus sur ce framework, la documentation est disponible pour vous guider. Vous pouvez consulter la mise en œuvre complète de l'application de blog sur GitHub .

Source : https://blog.logrocket.com/build-full-stack-app-tetra/

   #tetra  #fullstack #python 

Créez Une Application Complète Avec Tetra
曾 俊

曾 俊

1660336200

使用 Tetra 构建全栈应用程序

大多数全栈应用程序将前端和后端代码分成不同的文件;大多数 Web 框架都是基于这种结构构建的。随着文件和代码行数的增加,它可能会增加代码库的复杂性,从而使调试变得更加困难。通过引入一个名为Tetra的框架,这些单独文件引起的复杂性被最小化。

本教程将向您介绍 Tetra 框架及其组件。您还将学习如何构建一个简单的全栈博客应用程序,该应用程序使用 Tetra 执行 CRUD 功能。

什么是利乐?

Tetra是一个全栈框架,在服务器端使用Django和Alpine.js 构建,用于执行前端逻辑。Tetra 允许您将前端和后端逻辑放在一个统一的位置,并降低应用程序中的代码复杂性。它使用称为组件类的类将后端实现与前端连接起来。

Tetra 组件

tetra 组件是一个代码单元,可将其 Python、HTML、CSS 和 JavaScript 逻辑作为单个 Python 文件中的实体来处理。如果您熟悉React 框架,则可以将其组件的行为比作 Tetra 组件,只是 React 组件仅执行前端功能。

组件可以相互依赖或相互独立。这意味着您可以从另一个组件调用一个组件或将其作为独立组件。您可以在此处阅读有关 tetra 组件的更多信息。

让我们构建一个 Tetra 博客应用程序

本教程的其余部分将指导您完成如何在 Django 应用程序中安装 Tetra,以及如何使用 Tetra 构建博客应用程序的分步流程。博客应用程序将从管理员的角度呈现,您可以在其中创建新帖子、更新现有帖子、删除帖子以及查看所有博客帖子。

该应用程序将不包括任何身份验证或授权层。目的是使其尽可能简单,同时专注于 Tetra 的核心功能。

先决条件

项目设置

第一步是为应用程序创建一个虚拟环境。在终端中运行以下命令来设置项目目录和虚拟环境:

mkdir tetra
cd tetra
python -m venv tetra 
cd tetra
Scripts/activate

下一步是安装 Django。由于 Tetra 在 Django 框架上运行,因此需要将 Django 集成到您的应用程序中。

pip install django
django-admin startproject tetra_blog
cd tetra_blog

接下来,创建博客应用程序:

python manage.py startapp blog

将博客应用添加到文件中的INSTALLED_APPS列表中settings.py,如下图:

INSTALLED_APPS = [
    'blog.apps.BlogConfig',
    ...
 ]

在 app 目录中,创建一个components.py文件,该文件将包含您将在项目中构建的所有组件。

Tetra 安装和配置

成功设置 Django 项目后,下一步是在您的应用程序中安装 Tetra 框架。

pip install tetraframework

settings.py文件中,添加tetraINSTALLED_APPS列表中,如下图:

INSTALLED_APPS = [
    ...
    'tetra',
    'django.contrib.staticfiles',
    ...
]

确保在元素tetra之前列出。django.contrib.staticfiles

接下来,您需要将其包含tetra.middleware.TetraMiddlewareMIDDLEWARE列表的末尾。这会将组件中的 JavaScript 和 CSS 添加到 HTML 模板中。

MIDDLEWARE = [ 
    ...
    'tetra.middleware.TetraMiddleware'
]

将以下修改应用于根urls.py文件,以通过您的公共方法公开 Tetra 的端点:

from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    ...
 
    path('tetra/', include('tetra.urls')),
  ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

安装 esbuild

Tetra 构建您的 JS/CSS 组件并使用esbuild打包它们。这使您可以将前端实现中可能发生的任何错误跟踪到源 Python 文件。

npm init
npm install esbuild

如果您使用的是 Windows 操作系统,则必须esbuildsettings.py文件中显式声明构建路径:

TETRA_ESBUILD_PATH = '<absolute-path-to-project-root-directory>/node_modules/.bin/esbuild.cmd'

博客文章模型

该应用程序将在博客文章上执行 CRUD 功能。该Post模型将包含三个属性:标题、内容和日期。

将以下代码添加到models.py文件中以设置Post模型:

from django.db import models
from django.utils import timezone
from django.urls import reverse


class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    date_posted = models.DateTimeField(default=timezone.now)


    def __str__(self):
        return self.title
    // generate a reverse url for the model
    def get_absolute_url(self):
        return reverse('post-detail', kwargs={'pk': self.id})

执行迁移命令为模型创建表:

python manage.py makemigrations
python manage.py migrate

组件_AddPost

该组件负责渲染 UI 以创建新帖子。它还将包含我们创建Post模型并将数据保存在数据库中所需的 Python 逻辑。

add_post.py在文件夹中创建文件components并将以下代码添加到文件中:

from sourcetypes import javascript, css, django_html
from tetra import Component, public, Library
from ..models import Post

default = Library()

@default.register
class AddPost(Component):
    title=public("")
    content=public("")

 
    def load(self):
        self.post = Post.objects.filter(id=0)


    @public
    def add_post(self, title, content):
        post = Post(
            title = title,
            content = content
        )
        post.save()

在上面的代码中,AddPost该类是 Tetra 组件类的子类,它是您构建自定义组件的基类。使用@default.register装饰器,您将AddPost组件注册到 Tetra 库。

titlecontent变量是组件的公共属性,每个都有一个空字符串的初始值。的值public attributes可用于模板、JavaScript 和服务器逻辑。

load方法在组件启动以及从保存状态恢复时运行。您可以将load方法视为组件的构造函数;它在您从模板调用组件时运行。

add_post方法是一个公共方法,它接收标题和内容作为参数以创建Post实例,然后将其保存到数据库中。就像公共属性一样,公共方法暴露给模板、JavaScript 和 Python。@public您可以通过在方法签名上方添加装饰器来将方法声明为公共方法。

以下是您应该作为组件的一部分包含在add_post.py文件中的 HTML 代码AddPost

template: django_html = """
   
    <div class="container">
        <h2>Add blog post</h2>
        <label> Title
        <em>*</em>
        </label>
        <input type="text" maxlength="255" x-model="title" name="title" placeholder="Input post title" required/>

        <label> Content
        <em>*</em>
        </label>
        <textarea rows="20" cols="80" x-model="content" name="content" placeholder="Input blog content" required /> </textarea>

        <button type="submit" @click="addPost(title, content)"><em>Submit</em></button>
    </div>
    
    """

title输入字段接收帖子标题并通过Alpine.js x-model 属性将其绑定到public 属性。同样,textarea接收博客文章的内容并将值绑定到组件的content公共属性。

使用按钮标签中的 Alpine.js@click指令,模板调用 JavaScriptaddPost方法:

script: javascript = """
    export default {

        addPost(title, content){
            this.add_post(title, content)   
        }
        
    }
    """

JavaScriptaddPost方法将从标题和内容获取的值作为参数传递给add_post组件的公共方法。您还可以add_post直接从上面的 HTML 模板调用公共方法。

在这里通过 JavaScript 函数传递它的目的是演示如何在 Tetra 组件中执行 JavaScript 操作。这对于您希望更好地控制用户行为的情况很有帮助,例如在用户单击按钮后可能禁用按钮以防止他们在处理以前的请求时发送多个请求。

这是样式模板的 CSS 代码:

style: css = """
    .container {
        display: flex;
        flex-direction: column;
        align-items: left;
        justify-content: center;
        border-style: solid;
        width: fit-content;
        margin: auto;
        margin-top: 50px;
        width: 50%;
        border-radius: 15px;
        padding: 30px;
    }

    input, textarea, label{
        margin-bottom: 30px;
        margin-left: 20px;
        ;
    }

    label {
        font-weight: bold;
    }

    input{
        height: 40px;
    }

    h2 {
        text-align: center;
    }

    button {
        width: 150px;
        padding: 10px;
        border-radius: 9px;
        border-style: none;
        background: green;
        color: white;
        margin: auto;
    }
    
    """

下一步是AddPost从 Django 视图模板调用组件。在您在本教程上一部分创建add_post.html的博客应用程序文件夹中创建一个文件。templates将以下代码段添加到文件中:

{% load tetra %}
<!Doctype html>
<html lang="en">
  <head>
    <title> Add post </title>
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
     <form enctype="multipart/form-data" method="POST">
      {% csrf_token %}
      {% @ add_post / %}
    </form>
  </body>
  </html>

该模板首先将 Tetra 模板标签加载到模板中。它通过{% load tetra %}代码顶部描述的命令来实现这一点。您还需要分别通过{% tetra_styles %}和将 CSS 和 JavaScript 注入到模板中。{% tetra_scripts}

默认情况下,Tetra 不会在您的模板中包含 Alpine.js。您必须include_alpine=True在注入组件的 JavaScript 时通过添加显式声明它。

{% @ add_post / %}标签内的标签form调用组件的load方法,AddPost并渲染你在创建组件时在上面声明的HTML内容。

请注意,用于加载组件的组件名称是蛇形的。这是从模板调用组件的默认配置。您还可以在创建组件时设置自定义名称,如下所示:

...
@default.register(name="custom_component_name")
class AddPost(Component):
...

然后您可以使用{% @ custom_component_name / %}.

接下来,将以下代码段添加到views.py文件中:

from django.shortcuts import render

def add_post(request):
    return render(request, 'add_post.html')

在博客应用程序目录中创建一个urls.py文件,并将以下代码段添加到该文件中:

from django.urls import path
from . import views


urlpatterns = [
     path("add", views.add_post, name='add-post'),

]

在根urls.py文件中,添加以下路径:

urlpatterns = [
    ...
    path('tetra/', include('tetra.urls')),
    path('post/', include('blog.urls'))
]

使用 运行应用程序python manage.py runserver command。通过浏览器查看页面localhost:8000/post/add

这是页面的输出:

添加带有标题框和内容框的博客文章页面

组件_PostItem

PostItem组件包含用于在主屏幕上呈现创建的帖子的模板。

@default.register
class PostItem(Component):
    
    def load(self, post):
        self.post = post

load方法接收Post实例作为其参数并将其公开给在屏幕上呈现其标题和内容的 HTML 模板。

 template: django_html = """
   
    <article class="post-container" {% ... attrs %}>
            <small class="article-metadata">{{ post.date_posted.date}}</small>
            <p class="article-title"> {{ post.title }}</p>
            <p class="article-content">{{ post.content }}</p>

        </article>
            
    """

{% ... attrs %}标签是一个Tetra 属性标签,模板在调用组件时使用它来接收传递给它的参数。当使用属性标签接收参数时,您应该在 HTML 模板的根节点中声明该标签,就像在上面片段中的文章标签中所做的那样。

这是模板的 CSS 实现:

style: css = """
    
    .article-metadata {
        padding-bottom: 1px;
        margin-bottom: 4px;
        border-bottom: 1px solid #e3e3e3;
        
    }


    .article-title{
        font-size: x-large;
        font-weight: 700;
    }

    .article-content {
        white-space: pre-line;
    }

    .post-container{
        margin: 50px;
    }

    a.article-title:hover {
        color: #428bca;
        text-decoration: none;
    }

    .article-content {
        white-space: pre-line;
    }

    a.nav-item{
        text-align: right;
        margin-right: 100px;
    }

    h1 {
       text-align: center;
    }
    """

以下是通过该PostItem组件发布的帖子:

PostItem 组件显示带有默认文本的帖子

组件_ViewPosts

该组件负责渲染所有创建的帖子。将以下代码段添加到components.py文件中:

@default.register
class PostView(Component):

    def load(self):
        self.posts = Post.objects.all()

    template: django_html = """
        <div>
            <h1> Tetra blog </h1>
            <div class="navbar-nav">
                <a class="nav-item nav-link" href="{% url 'add-post' %}">New Post</a>
            <div>
            <div class="list-group">
                {% for post in posts %}
                    {% @ post_item post=post key=post.id / %}
                {% endfor %}
            </div>
         </div>
        """

组件中的load方法从数据库中检索所有创建的帖子。HTML 模板包含指向add-postURL 以创建新帖子的锚标记。

对于从数据库中获取的每个帖子,HTMLPostItem通过在 for 循环中将帖子对象作为其参数传递来创建一个组件。

接下来,ViewPost从 Django 视图模板调用组件。home.html在博客应用程序的文件夹中创建一个文件,templates并将以下代码段添加到该文件中:

{% load tetra %}
<!Doctype html>
<html lang="en">
  <head>
    <title> Blog home </title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
      {% @ view_post / %}
  </body>
  </html>

接下来,将以下内容添加到views.py文件中:

def home(request):
    return render(request, 'home.html')

最后,更新urlpatterns博客应用urls.py文件中的列表。

urlpatterns = [
     path("", views.home, name='home'),
    ...
]

您可以通过查看页面localhost:8000/post

利乐博客

组件_PostDetail

该组件将在单个页面上呈现完整的帖子。该页面还将包含两个按钮:一个用于删除和更新帖子。将以下代码添加到components.py文件中:

@default.register
class PostDetail(Component):
 
    def load(self, pk):
        self.post = Post.objects.filter(id=pk)[0]

    @public(update=False)
    def delete_item(self):
        Post.objects.filter(id=self.post.id).delete()
        self.client._removeComponent()

load方法id通过变量接收帖子的pk并获取Post实例,其 ID 与pk数据库中的值匹配。

delete_item方法Post从数据库中删除实例,自动将其从主屏幕中删除。默认情况下,公共方法会在您调用它时重新渲染组件。通过在装饰器中将update属性设置为,它确保它不会尝试重新呈现以前删除的帖子。False@public

这是 HTML 模板:

 template: django_html = """
        <article > 
            <small class="text-muted">{{ post.date_posted.date}}</small>
                
            <h2 class="article-title">{{ post.title }}</h2>
            <p class="article-content">{{ post.content }}</p>

            <div class="post-buttons">
            <button id="delete-button" type="submit" @click="delete_item()"><em>Delete</em></button>
           <button id="update-button"> <em>Update</em> </button>
            </div>
           
        </article>
    """

模板检索从该load方法获取的帖子的日期、标题和内容,并呈现这些值。它还包含用于删除和更新帖子的按钮。Delete按钮调用该方法delete_item对帖子执行删除操作。我们将在下一节中实现更新按钮。

这是模板的 CSS:

 style: css = """

        article{
            margin: 100px;
        }

        .post-buttons{
            position: absolute;
            right: 0;
        }

        #delete-button, #update-button{
            width: 150px;
            padding: 10px;
            border-radius: 9px;
            border-style: none;
            font-weight: bold;
            margin: auto;
        }

        #update-button{
            background: blue;
            color: white;
        }

        #delete-button{
            background: red;
            color: white;
        }

    """

PostItem上一节中创建的模板中,通过包含一个anchor标签来更新 HTML 代码,该标签将用户从主屏幕引导到帖子详细信息页面。

@default.register
class PostItem(Component):
   
  ...        
   
    template: django_html = """
   
    <article class="post-container" >
           ...
            <a href="{% url 'post-detail' pk=post.id %}"> {{ post.title }}</a>
          ...

        </article>
            
    """

在模板文件夹中,创建一个post-detail.html文件,该文件将用作后期详细信息页面的根 HTML 文件,并在文件中包含以下代码:

接下来,更新views.pyurls.py文件以包含帖子详细信息页面的路径:

def post_detail(request, **kwargs):
    return render(request, 'post_detail.html', kwargs)urlpatterns = [
     path("<int:pk>/", views.post_detail, name='post-detail')
]

通过单击博客主页上的帖子标题,在浏览器中查看帖子详细信息。

使用删除和更新按钮发布详细信息

组件_UpdatePost

该组件负责更新现有帖子的标题和内容。

@default.register
class PostUpdate(Component):
    title=public("")
    content=public("")


    def load(self, pk):
        self.post = Post.objects.filter(id=pk)[0]
        self.title=self.post.title
        self.content=self.post.content

    @public
    def update_post(self, title, content):
        self.post.title = title
        self.post.content = content

        self.post.save()

load方法接收您要更新的帖子的 ID 并从数据库中获取它。然后,它将其标题和内容分别分配给 thetitlecontentpublic 属性。

update_post方法接收更新的标题和内容并将它们分配给获取的帖子,然后将其保存到数据库中。

下面是组件的 HTML 模板:

 template: django_html = """
        <div class="container">
            <h2>Update blog post</h2>
            <label> Title
            <em>*</em>
            </label>
            <input type="text" maxlength="255" x-model="title" name="title" required/>

            <label> Content
            <em>*</em>
            </label>
            <textarea rows="20" cols="80" x-model="content" name="content" required> </textarea>

            <button type="submit" @click="update_post(title, content)"><em>Submit</em></button>
        </div>
        """

上面的模板通过 Alpine.jsx-model属性渲染了 title 和 content 公共属性的值,而按钮使用 Alpine.js@click函数调用该update_post方法并将 title 和 content 的新值作为参数传递。

PostDetail上一节中创建的模板中,通过包含一个anchor标签来更新 HTML 代码,该标签将用户从主屏幕引导到发布更新页面。

@default.register
class PostDetail(Component):
 
   ...

    template: django_html = """
        <article  {% ... attrs %} > 
            ...
            <a class="nav-item nav-link" href="{% url 'update-post' pk=post.id %}"><button id="update-button"> <em>Update</em> </button></a>
         ...
           
        </article>
    """

接下来,在模板文件夹中,创建一个post_update.html文件作为PostUpdate组件的根 HTML 模板。将以下代码段添加到文件中:

{% load tetra %}
<!Doctype html>
<html>
  <head>
    <title> Update post </title>
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
     <form enctype="multipart/form-data" method="POST">
      {% csrf_token %}
      {% @ post_update pk=pk / %}
    </form>
  </body>
  </html>

最后,分别用以下代码更新views.py和文件:urls.py

def update_post(request, **kwargs):
    return render(request, 'post_update.html', kwargs)urlpatterns = [
     ...
     path("<int:pk>", views.update_post, name='update-post'),
     ...

]

您可以通过单击帖子详细信息屏幕上的更新按钮导航到该update-post页面。

使用标题和内容框更新博客文章屏幕

博客文章标题现在显示“-updated”

关于 Tetra 的生产准备情况的说明

在撰写本文时,Tetra 仍处于早期开发阶段,目前支持 Python 3.9 及更高版本。然而,Tetra 团队正在努力将此框架的功能扩展到旧版本的 Python。

在开始使用 Tetra 进行生产之前,您应该知道的一件事是框架文档需要大量改进。它太简洁了,因为一些依赖关系要么根本没有解释,要么不够详细。例如,文档没有讨论如何处理图像,这就是我们为此演示构建博客应用程序的原因。

直到我完成项目后,我才意识到框架并不像文档所呈现的那样复杂。

结论

本文向您介绍了 Tetra 及其组件。您通过构建一个执行 CRUD 操作的简单博客应用程序了解了 Tetra 如何从单个文件中运行和执行全栈操作。

Tetra 主页包含一些其他示例,说明如何使用 Tetra 构建一些简单的应用程序。如果您也有兴趣了解有关此框架的更多信息,可以使用文档来指导您。您可以在GitHub 上查看博客应用程序的完整实现。

来源:https ://blog.logrocket.com/build-full-stack-app-tetra/

   #tetra  #fullstack #python 

使用 Tetra 构建全栈应用程序
藤本  結衣

藤本 結衣

1660334400

Tetra でフルスタック アプリを構築する

ほとんどのフルスタック アプリケーションは、フロントエンド コードとバックエンド コードを別個のファイルに分離します。ほとんどの Web フレームワークは、この構造に基づいて構築されています。ファイル数とコード行数が増えると、コードベースが複雑になり、デバッグがさらに難しくなる可能性があります。これらの個別のファイルに起因する複雑さは、 Tetraと呼ばれるフレームワークの導入によって最小限に抑えられました。

このチュートリアルでは、Tetra フレームワークとそのコンポーネントについて紹介します。また、Tetra を使用して CRUD 機能を実行するシンプルなフルスタック ブログ アプリケーションを構築する方法も学びます。

テトラとは?

Tetraは、サーバー側のDjangoとフロントエンド ロジックを実行するAlpine.jsで構築されたフルスタック フレームワークです。Tetra を使用すると、フロントエンドとバックエンドのロジックを 1 つの場所に配置し、アプリケーションのコードの複雑さを軽減できます。Component クラスと呼ばれるクラスを使用して、バックエンドの実装をフロントエンドに接続します。

テトラ成分

テトラ コンポーネントは、Python、HTML、CSS、および JavaScript ロジックを単一の Python ファイル内のエンティティとして処理するコードの単位です。React フレームワークに精通している場合は、React コンポーネントがフロントエンド機能のみを実行することを除いて、そのコンポーネントの動作を Tetra コンポーネントに例えることができます。

コンポーネントは、相互に依存することも、独立することもできます。これは、あるコンポーネントを別のコンポーネントから呼び出したり、スタンドアロン コンポーネントとして使用したりできることを意味します。テトラ コンポーネントの詳細については、こちらを参照してください。

Tetra ブログアプリを作ろう

このチュートリアルの残りの部分では、Django アプリケーションに Tetra をインストールする方法と、Tetra を使用してブログ アプリを構築する方法の段階的なフローについて説明します。ブログ アプリは管理者の観点から表示され、新しい投稿の作成、既存の投稿の更新、投稿の削除、およびすべてのブログ投稿の表示を行うことができます。

アプリケーションには、認証または認可レイヤーは含まれません。目的は、Tetra のコア機能に焦点を当てながら、できるだけシンプルに保つことです。

前提条件

  • Djangoを使用したモノリシック アプリケーションの構築に関する習熟度
  • HTML、CSS、および JavaScript の実用的な知識
  • 適切な IDE またはテキスト エディタ
  • マシンに Python バージョン 3.9 以降がインストールされている
  • マシンにインストールされたnpm パッケージ マネージャー

プロジェクトのセットアップ

最初のステップは、アプリケーションの仮想環境を作成することです。ターミナルで次のコマンドを実行して、プロジェクト ディレクトリと仮想環境を設定します。

mkdir tetra
cd tetra
python -m venv tetra 
cd tetra
Scripts/activate

次のステップは、Django のインストールです。Tetra は Django フレームワークで動作するため、Django をアプリケーションに統合する必要があります。

pip install django
django-admin startproject tetra_blog
cd tetra_blog

次に、ブログ アプリを作成します。

python manage.py startapp blog

以下に示すように、ブログ アプリをファイルのINSTALLED_APPSリストに追加します。settings.py

INSTALLED_APPS = [
    'blog.apps.BlogConfig',
    ...
 ]

app ディレクトリ内components.pyに、プロジェクトでビルドするすべてのコンポーネントを含むファイルを作成します。

Tetra のインストールと構成

Django プロジェクトを正常にセットアップしたら、次のステップは Tetra フレームワークをアプリケーションにインストールすることです。

pip install tetraframework

settings.pyファイルで、以下に示すようtetraにリストに追加します。INSTALLED_APPS

INSTALLED_APPS = [
    ...
    'tetra',
    'django.contrib.staticfiles',
    ...
]

要素tetraの前にリストされていることを確認してください。django.contrib.staticfiles

次に、リストtetra.middleware.TetraMiddlewareの最後にを含めますMIDDLEWARE。これにより、コンポーネントの JavaScript と CSS が HTML テンプレートに追加されます。

MIDDLEWARE = [ 
    ...
    'tetra.middleware.TetraMiddleware'
]

以下の変更をルートurls.pyファイルに適用して、パブリック メソッドを介して Tetra のエンドポイントを公開します。

from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    ...
 
    path('tetra/', include('tetra.urls')),
  ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

esbuild をインストールする

Tetra は、JS/CSS コンポーネントをビルドし、esbuildを使用してそれらをパッケージ化します。これにより、フロントエンド実装で発生する可能性のあるエラーをソース Python ファイルまで追跡できます。

npm init
npm install esbuild

Windows OS を使用している場合はesbuildsettings.pyファイルでビルド パスを明示的に宣言する必要があります。

TETRA_ESBUILD_PATH = '<absolute-path-to-project-root-directory>/node_modules/.bin/esbuild.cmd'

ブログ投稿モデル

アプリケーションは、ブログ投稿に対して CRUD 機能を実行します。モデルは、タイトル、コンテンツ、日付のPost3 つの属性で構成されます。

次のコードをファイルに追加して、モデルmodels.pyを設定します。Post

from django.db import models
from django.utils import timezone
from django.urls import reverse


class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    date_posted = models.DateTimeField(default=timezone.now)


    def __str__(self):
        return self.title
    // generate a reverse url for the model
    def get_absolute_url(self):
        return reverse('post-detail', kwargs={'pk': self.id})

移行コマンドを実行して、モデルのテーブルを作成します。

python manage.py makemigrations
python manage.py migrate

コンポーネント_AddPost

このコンポーネントは、UI をレンダリングして新しい投稿を作成する役割を担います。Postまた、モデルを作成してデータベースにデータを永続化するために必要な Python ロジックも含まれます。

add_post.pyフォルダーにファイルを作成しcomponents、次のコードをファイルに追加します。

from sourcetypes import javascript, css, django_html
from tetra import Component, public, Library
from ..models import Post

default = Library()

@default.register
class AddPost(Component):
    title=public("")
    content=public("")

 
    def load(self):
        self.post = Post.objects.filter(id=0)


    @public
    def add_post(self, title, content):
        post = Post(
            title = title,
            content = content
        )
        post.save()

上記のコードでは、AddPostクラスは Tetra コンポーネント クラスのサブクラスであり、カスタム コンポーネントを構築する基本クラスです。@default.registerデコレータを使用して、AddPostコンポーネントを Tetra ライブラリに登録します。

titleおよびcontent変数は、コンポーネントのパブリック属性であり、それぞれの初期値は空の文字列です。の値はpublic attributes、テンプレート、JavaScript、およびサーバー ロジックで使用できます。

このloadメソッドは、コンポーネントの開始時、および保存された状態からの再開時に実行されます。loadメソッドは、コンポーネントのコンストラクターと考えることができます。テンプレートからコンポーネントを呼び出すと実行されます。

add_postメソッドは、タイトルとコンテンツを引数として受け取ってインスタンスを作成し、それをデータベースに保存するpublicメソッドですPostpublic 属性と同様に、public メソッドはテンプレート、JavaScript、および Python に公開されます。@publicメソッド シグネチャの上にデコレータを追加して、メソッドを public として宣言します。

コンポーネントadd_post.pyの一部としてファイルに含める必要がある HTML コードは次のとおりです。AddPost

template: django_html = """
   
    <div class="container">
        <h2>Add blog post</h2>
        <label> Title
        <em>*</em>
        </label>
        <input type="text" maxlength="255" x-model="title" name="title" placeholder="Input post title" required/>

        <label> Content
        <em>*</em>
        </label>
        <textarea rows="20" cols="80" x-model="content" name="content" placeholder="Input blog content" required /> </textarea>

        <button type="submit" @click="addPost(title, content)"><em>Submit</em></button>
    </div>
    
    """

入力フィールドは投稿のタイトルを受け取り、それをAlpine.js x-model プロパティtitleを介して public 属性にバインドします。同様に、はブログ投稿のコンテンツを受け取り、その値をコンポーネントのpublic 属性にバインドします。textareacontent

@clickボタン タグ内でAlpine.js ディレクティブを使用して、テンプレートは JavaScriptaddPostメソッドを呼び出します。

script: javascript = """
    export default {

        addPost(title, content){
            this.add_post(title, content)   
        }
        
    }
    """

JavaScriptaddPostメソッドは、タイトルとコンテンツから取得した値を引数としてadd_postコンポーネントの public メソッドに渡します。add_post上記の HTML テンプレートから public メソッドを直接呼び出すこともできます。

ここで JavaScript 関数を介して渡す目的は、Tetra コンポーネント内で JavaScript 操作を実行する方法を示すことです。これは、ユーザーがボタンをクリックした後にボタンを無効にして、前のリクエストの処理中に複数のリクエストが送信されないようにするなど、ユーザーの動作をより細かく制御したい場合に役立ちます。

テンプレートのスタイルを設定する CSS コードは次のとおりです。

style: css = """
    .container {
        display: flex;
        flex-direction: column;
        align-items: left;
        justify-content: center;
        border-style: solid;
        width: fit-content;
        margin: auto;
        margin-top: 50px;
        width: 50%;
        border-radius: 15px;
        padding: 30px;
    }

    input, textarea, label{
        margin-bottom: 30px;
        margin-left: 20px;
        ;
    }

    label {
        font-weight: bold;
    }

    input{
        height: 40px;
    }

    h2 {
        text-align: center;
    }

    button {
        width: 150px;
        padding: 10px;
        border-radius: 9px;
        border-style: none;
        background: green;
        color: white;
        margin: auto;
    }
    
    """

AddPost次のステップは、Django ビュー テンプレートからコンポーネントを呼び出すことです。このチュートリアルの前のセクションで作成しadd_post.htmlたブログ アプリ フォルダーにファイルを作成します。templates次のスニペットをファイルに追加します。

{% load tetra %}
<!Doctype html>
<html lang="en">
  <head>
    <title> Add post </title>
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
     <form enctype="multipart/form-data" method="POST">
      {% csrf_token %}
      {% @ add_post / %}
    </form>
  </body>
  </html>

このテンプレートは、Tetra テンプレート タグをテンプレートにロードすることから始まります。{% load tetra %}これは、コードの上部に示されているコマンドによって実現されます。{% tetra_styles %}また、CSS と JavaScript をそれぞれとを介してテンプレートに挿入する必要があります{% tetra_scripts}

デフォルトでは、Tetra はテンプレートに Alpine.js を含めません。include_alpine=Trueコンポーネントの JavaScript を挿入するときに追加して、明示的に宣言する必要があります。

{% @ add_post / %}タグ内のタグは、コンポーネントのメソッドをform呼び出し、コンポーネントの作成時に上で宣言した HTML コンテンツをレンダリングします。loadAddPost

コンポーネントのロードに使用されるコンポーネント名がスネークケースになっていることに注意してください。これは、テンプレートからコンポーネントを呼び出すためのデフォルトの構成です。以下に示すように、コンポーネントを作成するときにカスタム名を設定することもできます。

...
@default.register(name="custom_component_name")
class AddPost(Component):
...

その後、 を使用してコンポーネントをロードできます{% @ custom_component_name / %}

次に、以下のスニペットをviews.pyファイルに追加します。

from django.shortcuts import render

def add_post(request):
    return render(request, 'add_post.html')

ブログ アプリ ディレクトリにファイルを作成しurls.py、次のスニペットをファイルに追加します。

from django.urls import path
from . import views


urlpatterns = [
     path("add", views.add_post, name='add-post'),

]

ルートurls.pyファイルに、次のパスを追加します。

urlpatterns = [
    ...
    path('tetra/', include('tetra.urls')),
    path('post/', include('blog.urls'))
]

でアプリケーションを実行しますpython manage.py runserver command。からブラウザでページを表示しますlocalhost:8000/post/add

ページの出力は次のとおりです。

タイトル ボックスとコンテンツ ボックスを含むブログ投稿ページを追加する

コンポーネント_PostItem

このPostItemコンポーネントには、作成した投稿をホーム画面に表示するためのテンプレートが含まれています。

@default.register
class PostItem(Component):
    
    def load(self, post):
        self.post = post

このloadメソッドはPostインスタンスを引数として受け取り、タイトルとコンテンツを画面にレンダリングする HTML テンプレートに公開します。

 template: django_html = """
   
    <article class="post-container" {% ... attrs %}>
            <small class="article-metadata">{{ post.date_posted.date}}</small>
            <p class="article-title"> {{ post.title }}</p>
            <p class="article-content">{{ post.content }}</p>

        </article>
            
    """

{% ... attrs %}タグは、テンプレートがコンポーネントの呼び出し時にテンプレートに渡された引数を受け取るために使用するTetra属性タグです。attributes タグを使用して引数を受け取る場合、上記のスニペットの article タグで行ったように、HTML テンプレートのルート ノードでタグを宣言する必要があります。

テンプレートの CSS 実装は次のとおりです。

style: css = """
    
    .article-metadata {
        padding-bottom: 1px;
        margin-bottom: 4px;
        border-bottom: 1px solid #e3e3e3;
        
    }


    .article-title{
        font-size: x-large;
        font-weight: 700;
    }

    .article-content {
        white-space: pre-line;
    }

    .post-container{
        margin: 50px;
    }

    a.article-title:hover {
        color: #428bca;
        text-decoration: none;
    }

    .article-content {
        white-space: pre-line;
    }

    a.nav-item{
        text-align: right;
        margin-right: 100px;
    }

    h1 {
       text-align: center;
    }
    """

PostItemコンポーネントを介した投稿は次のようになります。

PostItem コンポーネントはデフォルトのテキストで投稿を表示します

コンポーネント_ViewPosts

このコンポーネントは、作成されたすべての投稿のレンダリングを担当します。次のスニペットをcomponents.pyファイルに追加します。

@default.register
class PostView(Component):

    def load(self):
        self.posts = Post.objects.all()

    template: django_html = """
        <div>
            <h1> Tetra blog </h1>
            <div class="navbar-nav">
                <a class="nav-item nav-link" href="{% url 'add-post' %}">New Post</a>
            <div>
            <div class="list-group">
                {% for post in posts %}
                    {% @ post_item post=post key=post.id / %}
                {% endfor %}
            </div>
         </div>
        """

コンポーネントのloadメソッドは、作成されたすべての投稿をデータベースから取得します。add-postHTML テンプレートには、新しい投稿を作成するための URL に誘導するアンカー タグが含まれています。

データベースからフェッチされた各投稿に対して、HTML はPostItemfor ループ内の引数として投稿オブジェクトを渡すことによってコンポーネントを作成します。

ViewPost次に、 Django ビュー テンプレートからコンポーネントを呼び出します。home.htmlブログ アプリのフォルダーにファイルを作成しtemplates、次のスニペットをファイルに追加します。

{% load tetra %}
<!Doctype html>
<html lang="en">
  <head>
    <title> Blog home </title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
      {% @ view_post / %}
  </body>
  </html>

次に、views.pyファイルに次を追加します。

def home(request):
    return render(request, 'home.html')

最後にurlpatterns、ブログ アプリurls.pyファイルのリストを更新します。

urlpatterns = [
     path("", views.home, name='home'),
    ...
]

からページを表示できますlocalhost:8000/post

テトラブログ

コンポーネント_PostDetail

このコンポーネントは、投稿全体を 1 つのページに表示します。このページには、投稿の削除と更新用の 2 つのボタンも含まれます。components.py次のコードをファイルに追加します。

@default.register
class PostDetail(Component):
 
    def load(self, pk):
        self.post = Post.objects.filter(id=pk)[0]

    @public(update=False)
    def delete_item(self):
        Post.objects.filter(id=self.post.id).delete()
        self.client._removeComponent()

このloadメソッドは、変数をid介して投稿の を受け取り、IDがデータベースの値と一致するインスタンスを取得します。pkPostpk

このdelete_itemメソッドは、Postインスタンスをデータベースから削除し、ホーム画面から自動的に削除します。デフォルトでは、パブリック メソッドは、呼び出し時にコンポーネントを再レンダリングします。updateプロパティをデコレータで に設定することで、以前に削除された投稿を再レンダリングしようとしないようにしFalseます。@public

HTML テンプレートは次のとおりです。

 template: django_html = """
        <article > 
            <small class="text-muted">{{ post.date_posted.date}}</small>
                
            <h2 class="article-title">{{ post.title }}</h2>
            <p class="article-content">{{ post.content }}</p>

            <div class="post-buttons">
            <button id="delete-button" type="submit" @click="delete_item()"><em>Delete</em></button>
           <button id="update-button"> <em>Update</em> </button>
            </div>
           
        </article>
    """

テンプレートは、メソッドからフェッチされた投稿の日付、タイトル、およびコンテンツを取得し、loadこれらの値をレンダリングします。また、投稿を削除および更新するためのボタンも含まれています。[削除]delete_itemボタンは、投稿に対して削除操作を実行するメソッドを呼び出します。次のセクションで [更新] ボタンを実装します。

テンプレートの CSS は次のとおりです。

 style: css = """

        article{
            margin: 100px;
        }

        .post-buttons{
            position: absolute;
            right: 0;
        }

        #delete-button, #update-button{
            width: 150px;
            padding: 10px;
            border-radius: 9px;
            border-style: none;
            font-weight: bold;
            margin: auto;
        }

        #update-button{
            background: blue;
            color: white;
        }

        #delete-button{
            background: red;
            color: white;
        }

    """

前のセクションで作成したテンプレートで、ユーザーをホーム画面から投稿の詳細ページに誘導PostItemするタグを含めて、HTML コードを更新します。anchor

@default.register
class PostItem(Component):
   
  ...        
   
    template: django_html = """
   
    <article class="post-container" >
           ...
            <a href="{% url 'post-detail' pk=post.id %}"> {{ post.title }}</a>
          ...

        </article>
            
    """

テンプレート フォルダーで、post-detail.html詳細後のページのルート HTML ファイルとして機能するファイルを作成し、ファイルに次のコードを含めます。

views.py次に、ファイルとファイルを更新して、urls.py詳細後のページへのパスを含めます。

def post_detail(request, **kwargs):
    return render(request, 'post_detail.html', kwargs)urlpatterns = [
     path("<int:pk>/", views.post_detail, name='post-detail')
]

ブログのホームページから投稿のタイトルをクリックして、ブラウザーで投稿の詳細を表示します。

削除ボタンと更新ボタンで詳細を投稿

コンポーネント_UpdatePost

このコンポーネントは、既存の投稿のタイトルとコンテンツを更新する役割を果たします。

@default.register
class PostUpdate(Component):
    title=public("")
    content=public("")


    def load(self, pk):
        self.post = Post.objects.filter(id=pk)[0]
        self.title=self.post.title
        self.content=self.post.content

    @public
    def update_post(self, title, content):
        self.post.title = title
        self.post.content = content

        self.post.save()

このloadメソッドは、更新する投稿の ID を受け取り、データベースから取得します。次に、そのタイトルとコンテンツをそれぞれパブリック属性titlecontentパブリック属性に割り当てます。

このupdate_postメソッドは、更新されたタイトルとコンテンツを受け取り、それらを取得した投稿に割り当ててから、データベースに保存します。

以下は、コンポーネントの HTML テンプレートです。

 template: django_html = """
        <div class="container">
            <h2>Update blog post</h2>
            <label> Title
            <em>*</em>
            </label>
            <input type="text" maxlength="255" x-model="title" name="title" required/>

            <label> Content
            <em>*</em>
            </label>
            <textarea rows="20" cols="80" x-model="content" name="content" required> </textarea>

            <button type="submit" @click="update_post(title, content)"><em>Submit</em></button>
        </div>
        """

上記のテンプレートは、Alpine.jsx-modelプロパティを介してタイトルとコンテンツのパブリック属性の値をレンダリングしますが、ボタンは Alpine.js@click関数を使用してupdate_postメソッドを呼び出し、タイトルとコンテンツの新しい値を引数として渡します。

前のセクションで作成したPostDetailテンプレートでanchor、ユーザーをホーム画面から更新後のページに誘導するタグを含めて、HTML コードを更新します。

@default.register
class PostDetail(Component):
 
   ...

    template: django_html = """
        <article  {% ... attrs %} > 
            ...
            <a class="nav-item nav-link" href="{% url 'update-post' pk=post.id %}"><button id="update-button"> <em>Update</em> </button></a>
         ...
           
        </article>
    """

次に、テンプレート フォルダー内にpost_update.html、コンポーネントのルート HTML テンプレートとして機能するファイルを作成しますPostUpdate。次のスニペットをファイルに追加します。

{% load tetra %}
<!Doctype html>
<html>
  <head>
    <title> Update post </title>
    {% tetra_styles %}
    {% tetra_scripts include_alpine=True %}
  </head>
  <body>
     <form enctype="multipart/form-data" method="POST">
      {% csrf_token %}
      {% @ post_update pk=pk / %}
    </form>
  </body>
  </html>

views.py最後に、とurls.pyファイルをそれぞれ次のコードで更新します。

def update_post(request, **kwargs):
    return render(request, 'post_update.html', kwargs)urlpatterns = [
     ...
     path("<int:pk>", views.update_post, name='update-post'),
     ...

]

投稿詳細画面の「更新update-post」ボタンからページに移動できます。

タイトルとコンテンツ ボックスでブログ投稿画面を更新する

ブログ投稿のタイトルに「-updated」と表示されるようになりました

Tetra の生産準備に関する注意事項

この記事を書いている時点では、Tetra はまだ開発の初期段階にあり、現在 Python 3.9 以降をサポートしています。ただし、Tetra チームは、このフレームワークの機能を古いバージョンの Python に拡張する作業を行っています。

Tetra を使用して生産を開始する前に知っておくべきことの 1 つは、フレームワークのドキュメントを大幅に改善する必要があるということです。一部の依存関係がまったく説明されていないか、十分に詳しく説明されていなかったため、簡潔すぎました。たとえば、このドキュメントでは画像の処理方法について説明していないため、このデモ用にブログ アプリを作成しました。

プロジェクトを完了してから、フレームワークがドキュメントにあるほど複雑ではないことに気付きました。

結論

この記事では、Tetra とそのコンポーネントについて紹介しました。CRUD 操作を実行する単純なブログ アプリを構築することで、Tetra がどのように機能し、単一のファイルからフルスタック操作を実行するかを学習しました。

Tetra ホームページには、Tetra を使用していくつかの単純なアプリケーションを構築する方法の追加の例が含まれています。このフレームワークについて詳しく知りたい場合は、ドキュメントを参照してください。GitHubでブログ アプリの完全な実装を確認できます。

ソース: https://blog.logrocket.com/build-full-stack-app-tetra/

  #tetra  #fullstack #python 

Tetra でフルスタック アプリを構築する
Anissa  Barrows

Anissa Barrows

1659948360

Hipchat Rb: HipChat HTTP API Wrapper in Ruby with Capistrano Hooks

HipChat Wrapper

A very basic wrapper for the HipChat HTTP API.

CI Status

Requirements

  • Ruby 2.0.0 or higher
  • HipChat Account, sign up here!

Installation

Gemfile

gem 'hipchat'

Install

gem install hipchat

Usage

API v1

client = HipChat::Client.new(api_token, :api_version => 'v1')
# Set http proxy
client = HipChat::Client.new(api_token, :api_version => 'v1', :http_proxy => 'http://proxy_host:proxy_port')

# 'username' is the name for which the message will be presented as from
client['my room'].send('username', 'I talk')

# Send notifications to users (default false)
client['my room'].send('username', 'I quit!', :notify => true)

# Color it red. or "yellow", "green", "purple", "random" (default "yellow")
client['my room'].send('username', 'Build failed!', :color => 'red')

# Have your message rendered as text in HipChat (see https://www.hipchat.com/docs/api/method/rooms/message)
client['my room'].send('username', '@coworker Build faild!', :message_format => 'text')

# Update the topic of a room in HipChat (see https://www.hipchat.com/docs/api/method/rooms/topic)
client['my room'].topic('Free Ice Cream in the kitchen')

# Change the from field for a topic update (default "API")
client['my room'].topic('Weekely sales: $10,000', :from => 'Sales Team')

# Get history from a room
client['my room'].history()

# Get history for a date in time with a particular timezone (default is latest 75 messages, timezone default is 'UTC')
client['my room'].history(:date => '2010-11-19', :timezone => 'PST')

# Create a new room, V1 requires owner_user_id (see https://www.hipchat.com/docs/api/method/rooms/create)
client.create_room("Name", :owner_user_id => 'user_id')

API v2

client = HipChat::Client.new(api_token)
# Set http proxy
client = HipChat::Client.new(api_token, :http_proxy => 'http://proxy_host:proxy_port')

# 'username' is the name for which the message will be presented as from
client['my room'].send('username', 'I talk')

# Send notifications to users (default false)
client['my room'].send('username', 'I quit!', :notify => true)

# Color it red. or "yellow", "green", "purple", "random" (default "yellow")
client['my room'].send('username', 'Build failed!', :color => 'red')

# Have your message rendered as text in HipChat (see https://www.hipchat.com/docs/apiv2/method/send_room_notification)
client['my room'].send('username', '@coworker Build faild!', :message_format => 'text')

# Update the topic of a room in HipChat (see https://www.hipchat.com/docs/apiv2/method/set_topic)
client['my room'].topic('Free Ice Cream in the kitchen')

# Change the from field for a topic update (default "API")
client['my room'].topic('Weekely sales: $10,000', :from => 'Sales Team')

# Get history from a room
client['my room'].history()

# Get history for a date in time with a particular timezone (default is latest 75 messages, timezone default is 'UTC')
client['my room'].history(:date => '2010-11-19', :timezone => 'PST')

# Get statistics from a room
client['my room'].statistics()

# Create a new room (see https://www.hipchat.com/docs/apiv2/method/create_room)
client.create_room("Name", options = {})

# Get room data (see https://www.hipchat.com/docs/apiv2/method/get_room)
client['my room'].get_room

# Update room data (see https://www.hipchat.com/docs/apiv2/method/update_room)
It's recommended to call client['my room'].get_room then pass in modified hash attributes to #update_room
client['my room'].update_room(options = {})

# Delete room (see https://www.hipchat.com/docs/apiv2/method/delete_room)
client['my room'].delete_room

# Invite user to room (see https://www.hipchat.com/docs/apiv2/method/invite_user)
client['my room'].invite("USER_ID_OR_NAME", options = {})

# Sends a user a private message. Valid value for user are user id or email address
client.user('foo@bar.org').send('I can send private messages')

# Update a user status.  Available options for show are 'away', 'chat', 'dnd', 'xa'
client.user('foo@bar.org').status('this is my status',
        :name=>'Foo Bar',
        :status=>'Doing very important stuff',
        :show=>'xa',
        :mention_name=>'foo',
        :email=>'foo@barr.org')

Custom Server URL

client = HipChat::Client.new(api_token, :server_url => 'https://domain.com')
# 'username' is the name for which the message will be presented as from
client['my room'].send('username', 'I talk')

# Send notifications to users (default false)
client['my room'].send('username', 'I quit!', :notify => true)

# Color it red. or "yellow", "green", "purple", "random" (default "yellow")
client['my room'].send('username', 'Build failed!', :color => 'red')

# Have your message rendered as text in HipChat (see https://www.hipchat.com/docs/apiv2/method/send_room_notification)
client['my room'].send('username', '@coworker Build faild!', :message_format => 'text')

# Update the topic of a room in HipChat (see https://www.hipchat.com/docs/apiv2/method/set_topic)
client['my room'].topic('Free Ice Cream in the kitchen')

# Change the from field for a topic update (default "API")
client['my room'].topic('Weekely sales: $10,000', :from => 'Sales Team')

# Get history from a room
client['my room'].history()

# Get history for a date in time with a particular timezone (default is latest 75 messages, timezone default is 'UTC')
client['my room'].history(:date => '2010-11-19', :timezone => 'PST')

# Create a new room (see https://www.hipchat.com/docs/apiv2/method/create_room)
client.create_room("Name", options = {})

# Get room data (see https://www.hipchat.com/docs/apiv2/method/get_room)
client['my room'].get_room

# Update room data (see https://www.hipchat.com/docs/apiv2/method/update_room)
It's easiest to call client['my room'].get_room, parse the json and then pass in modified hash attributes
client['my room'].update_room(options = {})

# Invite user to room (see https://www.hipchat.com/docs/apiv2/method/invite_user)
client['my room'].invite("USER_ID_OR_NAME", options = {})

# Sends a user a private message. Valid value for user are user id or email address
client.user('foo@bar.org').send('I can send private messages')

Capistrano

Capfile

require 'hipchat/capistrano'

deploy.rb

# Required
set :hipchat_token, "<your token>"
set :hipchat_room_name, "Your room" # If you pass an array such as ["room_a", "room_b"] you can send announcements to multiple rooms.
# Optional
set :hipchat_enabled, true # set to false to prevent any messages from being sent
set :hipchat_announce, false # notify users
set :hipchat_color, 'yellow' #normal message color
set :hipchat_success_color, 'green' #finished deployment message color
set :hipchat_failed_color, 'red' #cancelled deployment message color
set :hipchat_message_format, 'html' # Sets the deployment message format, see https://www.hipchat.com/docs/api/method/rooms/message
set :hipchat_options, {
  :api_version  => "v2" # Set "v2" to send messages with API v2
}

Who did it?

To determine the user that is currently running the deploy, the capistrano tasks will look for the following:

  1. The $HIPCHAT_USER environment variable
  2. The hipchat_human capistrano var.
  3. The git user.name var.
  4. The $USER environment variable.

Commit log notification (only for Capistrano 2)

Send commit log with deploy notification. We currently support git and svn.

set :hipchat_commit_log, true
# Optional
set :hipchat_commit_log_format, ":time :user\n:message\n"
set :hipchat_commit_log_time_format, "%Y/%m/%d %H:%M:%S"
set :hipchat_commit_log_message_format, "^PROJECT-\d+" # extracts ticket number from message

Rails 3 Rake Task

Send a message using a rake task:

rake hipchat:send["hello world"]

or

rake hipchat:send MESSAGE="hello world"

Options like the room, API token, user name and notification flag can be set in YAML.

RAILS_ROOT/config/hipchat.yml:

token: "<your token>"
room: ["Room name(s) or id(s)"] # this is an array
user: "Your name" # Default to `whoami`
notify: true # Defaults to false
api_version: "v2" # optional, defaults to v2
color: "yellow"

Engine Yard

Use a deploy hook to send messages from Engine Yard’s Cloud platform.

RAILS_ROOT/deploy/after_restart.rb:

on_app_master do
  message  = "Deploying revision #{config.active_revision} to #{config.environment_name}"
  message += " (with migrations)" if config.migrate?
  message += "."

  # Send a message via rake task assuming a hipchat.yml in your config like above
  run "cd #{config.release_path} && bundle exec rake hipchat:send MESSAGE='#{message}'"
end

Chef Handler

APIv1 ONLY, use APIv1 Key
NOTE: APIv2 required for HipChat Server & HipChat Data Center

Report on Chef runs.

Within a Recipe:

include_recipe 'chef_handler'

gem_package 'hipchat'

chef_handler 'HipChat::NotifyRoom' do
  action :enable
  arguments ['API_KEY', 'HIPCHAT_ROOM']
  source File.join(Gem.all_load_paths.grep(/hipchat/).first,
                   'hipchat', 'chef.rb')
end

With client.rb:

   require 'hipchat/chef'
   hipchat_handler = HipChat::NotifyRoom.new("API_KEY", "HIPCHAT_ROOM")
   exception_handlers << hipchat_handler

With HipChat Data Center and HipChat Server

Add an “options” hash to set your URL:

  • arguments [‘API_KEY’, ‘HIPCHAT_ROOM’, options={hipchat_options: {server_url: “https://hipchat.example.com”}}]
  • hipchat_handler = HipChat::NotifyRoom.new(“API_KEY”, “HIPCHAT_ROOM”, options={hipchat_options: {server_url: “https://hipchat.example.com”}})

Author: hipchat
Source code: https://github.com/hipchat/hipchat-rb
License: MIT license

#ruby 

Hipchat Rb: HipChat HTTP API Wrapper in Ruby with Capistrano Hooks

Builds ActiveRecord Named Scopes That Take Advantage Of PostgreSQL's

pg_search  

DESCRIPTION

PgSearch builds named scopes that take advantage of PostgreSQL's full text search.

Read the blog post introducing PgSearch at https://tanzu.vmware.com/content/blog/pg-search-how-i-learned-to-stop-worrying-and-love-postgresql-full-text-search

REQUIREMENTS

INSTALL

$ gem install pg_search

or add this line to your Gemfile:

gem 'pg_search'

Non-Rails projects

In addition to installing and requiring the gem, you may want to include the PgSearch rake tasks in your Rakefile. This isn't necessary for Rails projects, which gain the Rake tasks via a Railtie.

load "pg_search/tasks.rb"

USAGE

To add PgSearch to an Active Record model, simply include the PgSearch module.

class Shape < ActiveRecord::Base
  include PgSearch::Model
end

Multi-search vs. search scopes

pg_search supports two different techniques for searching, multi-search and search scopes.

The first technique is multi-search, in which records of many different Active Record classes can be mixed together into one global search index across your entire application. Most sites that want to support a generic search page will want to use this feature.

The other technique is search scopes, which allow you to do more advanced searching against only one Active Record class. This is more useful for building things like autocompleters or filtering a list of items in a faceted search.

Multi-search

Setup

Before using multi-search, you must generate and run a migration to create the pg_search_documents database table.

$ rails g pg_search:migration:multisearch
$ rake db:migrate

multisearchable

To add a model to the global search index for your application, call multisearchable in its class definition.

class EpicPoem < ActiveRecord::Base
  include PgSearch::Model
  multisearchable against: [:title, :author]
end

class Flower < ActiveRecord::Base
  include PgSearch::Model
  multisearchable against: :color
end

If this model already has existing records, you will need to reindex this model to get existing records into the pg_search_documents table. See the rebuild task below.

Whenever a record is created, updated, or destroyed, an Active Record callback will fire, leading to the creation of a corresponding PgSearch::Document record in the pg_search_documents table. The :against option can be one or several methods which will be called on the record to generate its search text.

You can also pass a Proc or method name to call to determine whether or not a particular record should be included.

class Convertible < ActiveRecord::Base
  include PgSearch::Model
  multisearchable against: [:make, :model],
                  if: :available_in_red?
end

class Jalopy < ActiveRecord::Base
  include PgSearch::Model
  multisearchable against: [:make, :model],
                  if: lambda { |record| record.model_year > 1970 }
end

Note that the Proc or method name is called in an after_save hook. This means that you should be careful when using Time or other objects. In the following example, if the record was last saved before the published_at timestamp, it won't get listed in global search at all until it is touched again after the timestamp.

class AntipatternExample
  include PgSearch::Model
  multisearchable against: [:contents],
                  if: :published?

  def published?
    published_at < Time.now
  end
end

problematic_record = AntipatternExample.create!(
  contents: "Using :if with a timestamp",
  published_at: 10.minutes.from_now
)

problematic_record.published?     # => false
PgSearch.multisearch("timestamp") # => No results

sleep 20.minutes

problematic_record.published?     # => true
PgSearch.multisearch("timestamp") # => No results

problematic_record.save!

problematic_record.published?     # => true
PgSearch.multisearch("timestamp") # => Includes problematic_record

More Options

Conditionally update pg_search_documents

You can specify an :update_if parameter to conditionally update pg_search documents. For example:

multisearchable(
    against: [:body],
    update_if: :body_changed?
  )

Specify additional attributes to be saved on the pg_search_documents table

You can specify :additional_attributes to be saved within the pg_search_documents table. For example, perhaps you are indexing a book model and an article model and wanted to include the author_id.

First, we need to add a reference to author to the migration creating our pg_search_documents table.

  create_table :pg_search_documents do |t|
    t.text :content
    t.references :author, index: true
    t.belongs_to :searchable, polymorphic: true, index: true
    t.timestamps null: false
  end

Then, we can send in this additional attribute in a lambda

  multisearchable(
    against: [:title, :body],
    additional_attributes: -> (article) { { author_id: article.author_id } }
  )

This allows much faster searches without joins later on by doing something like:

PgSearch.multisearch(params['search']).where(author_id: 2)

NOTE: You must currently manually call record.update_pg_search_document for the additional attribute to be included in the pg_search_documents table

Multi-search associations

Two associations are built automatically. On the original record, there is a has_one :pg_search_document association pointing to the PgSearch::Document record, and on the PgSearch::Document record there is a belongs_to :searchable polymorphic association pointing back to the original record.

odyssey = EpicPoem.create!(title: "Odyssey", author: "Homer")
search_document = odyssey.pg_search_document #=> PgSearch::Document instance
search_document.searchable #=> #<EpicPoem id: 1, title: "Odyssey", author: "Homer">

Searching in the global search index

To fetch the PgSearch::Document entries for all of the records that match a given query, use PgSearch.multisearch.

odyssey = EpicPoem.create!(title: "Odyssey", author: "Homer")
rose = Flower.create!(color: "Red")
PgSearch.multisearch("Homer") #=> [#<PgSearch::Document searchable: odyssey>]
PgSearch.multisearch("Red") #=> [#<PgSearch::Document searchable: rose>]

Chaining method calls onto the results

PgSearch.multisearch returns an ActiveRecord::Relation, just like scopes do, so you can chain scope calls to the end. This works with gems like Kaminari that add scope methods. Just like with regular scopes, the database will only receive SQL requests when necessary.

PgSearch.multisearch("Bertha").limit(10)
PgSearch.multisearch("Juggler").where(searchable_type: "Occupation")
PgSearch.multisearch("Alamo").page(3).per(30)
PgSearch.multisearch("Diagonal").find_each do |document|
  puts document.searchable.updated_at
end
PgSearch.multisearch("Moro").reorder("").group(:searchable_type).count(:all)
PgSearch.multisearch("Square").includes(:searchable)

Configuring multi-search

PgSearch.multisearch can be configured using the same options as pg_search_scope (explained in more detail below). Just set the PgSearch.multisearch_options in an initializer:

PgSearch.multisearch_options = {
  using: [:tsearch, :trigram],
  ignoring: :accents
}

Rebuilding search documents for a given class

If you change the :against option on a class, add multisearchable to a class that already has records in the database, or remove multisearchable from a class in order to remove it from the index, you will find that the pg_search_documents table could become out-of-sync with the actual records in your other tables.

The index can also become out-of-sync if you ever modify records in a way that does not trigger Active Record callbacks. For instance, the #update_attribute instance method and the .update_all class method both skip callbacks and directly modify the database.

To remove all of the documents for a given class, you can simply delete all of the PgSearch::Document records.

PgSearch::Document.delete_by(searchable_type: "Animal")

To regenerate the documents for a given class, run:

PgSearch::Multisearch.rebuild(Product)

The rebuild method will delete all the documents for the given class before regenerating them. In some situations this may not be desirable, such as when you're using single-table inheritance and searchable_type is your base class. You can prevent rebuild from deleting your records like so:

PgSearch::Multisearch.rebuild(Product, clean_up: false)

rebuild runs inside a single transaction. To run outside of a transaction, you can pass transactional: false like so:

PgSearch::Multisearch.rebuild(Product, transactional: false)

Rebuild is also available as a Rake task, for convenience.

$ rake pg_search:multisearch:rebuild[BlogPost]

A second optional argument can be passed to specify the PostgreSQL schema search path to use, for multi-tenant databases that have multiple pg_search_documents tables. The following will set the schema search path to "my_schema" before reindexing.

$ rake pg_search:multisearch:rebuild[BlogPost,my_schema]

For models that are multisearchable :against methods that directly map to Active Record attributes, an efficient single SQL statement is run to update the pg_search_documents table all at once. However, if you call any dynamic methods in :against, the following strategy will be used:

PgSearch::Document.delete_all(searchable_type: "Ingredient")
Ingredient.find_each { |record| record.update_pg_search_document }

You can also provide a custom implementation for rebuilding the documents by adding a class method called rebuild_pg_search_documents to your model.

class Movie < ActiveRecord::Base
  belongs_to :director

  def director_name
    director.name
  end

  multisearchable against: [:name, :director_name]

  # Naive approach
  def self.rebuild_pg_search_documents
    find_each { |record| record.update_pg_search_document }
  end

  # More sophisticated approach
  def self.rebuild_pg_search_documents
    connection.execute <<~SQL.squish
     INSERT INTO pg_search_documents (searchable_type, searchable_id, content, created_at, updated_at)
       SELECT 'Movie' AS searchable_type,
              movies.id AS searchable_id,
              CONCAT_WS(' ', movies.name, directors.name) AS content,
              now() AS created_at,
              now() AS updated_at
       FROM movies
       LEFT JOIN directors
         ON directors.id = movies.director_id
    SQL
  end
end

Note: If using PostgreSQL before 9.1, replace the CONCAT_WS() function call with double-pipe concatenation, eg. (movies.name || ' ' || directors.name). However, now be aware that if any of the joined values is NULL then the final content value will also be NULL, whereas CONCAT_WS() will selectively ignore NULL values.

Disabling multi-search indexing temporarily

If you have a large bulk operation to perform, such as importing a lot of records from an external source, you might want to speed things up by turning off indexing temporarily. You could then use one of the techniques above to rebuild the search documents off-line.

PgSearch.disable_multisearch do
  Movie.import_from_xml_file(File.open("movies.xml"))
end

pg_search_scope

You can use pg_search_scope to build a search scope. The first parameter is a scope name, and the second parameter is an options hash. The only required option is :against, which tells pg_search_scope which column or columns to search against.

Searching against one column

To search against a column, pass a symbol as the :against option.

class BlogPost < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :search_by_title, against: :title
end

We now have an ActiveRecord scope named search_by_title on our BlogPost model. It takes one parameter, a search query string.

BlogPost.create!(title: "Recent Developments in the World of Pastrami")
BlogPost.create!(title: "Prosciutto and You: A Retrospective")
BlogPost.search_by_title("pastrami") # => [#<BlogPost id: 2, title: "Recent Developments in the World of Pastrami">]

Searching against multiple columns

Just pass an Array if you'd like to search more than one column.

class Person < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :search_by_full_name, against: [:first_name, :last_name]
end

Now our search query can match either or both of the columns.

person_1 = Person.create!(first_name: "Grant", last_name: "Hill")
person_2 = Person.create!(first_name: "Hugh", last_name: "Grant")

Person.search_by_full_name("Grant") # => [person_1, person_2]
Person.search_by_full_name("Grant Hill") # => [person_1]

Dynamic search scopes

Just like with Active Record named scopes, you can pass in a Proc object that returns a hash of options. For instance, the following scope takes a parameter that dynamically chooses which column to search against.

Important: The returned hash must include a :query key. Its value does not necessary have to be dynamic. You could choose to hard-code it to a specific value if you wanted.

class Person < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :search_by_name, lambda { |name_part, query|
    raise ArgumentError unless [:first, :last].include?(name_part)
    {
      against: name_part,
      query: query
    }
  }
end

person_1 = Person.create!(first_name: "Grant", last_name: "Hill")
person_2 = Person.create!(first_name: "Hugh", last_name: "Grant")

Person.search_by_name :first, "Grant" # => [person_1]
Person.search_by_name :last, "Grant" # => [person_2]

Searching through associations

It is possible to search columns on associated models. Note that if you do this, it will be impossible to speed up searches with database indexes. However, it is supported as a quick way to try out cross-model searching.

You can pass a Hash into the :associated_against option to set up searching through associations. The keys are the names of the associations and the value works just like an :against option for the other model. Right now, searching deeper than one association away is not supported. You can work around this by setting up a series of :through associations to point all the way through.

class Cracker < ActiveRecord::Base
  has_many :cheeses
end

class Cheese < ActiveRecord::Base
end

class Salami < ActiveRecord::Base
  include PgSearch::Model

  belongs_to :cracker
  has_many :cheeses, through: :cracker

  pg_search_scope :tasty_search, associated_against: {
    cheeses: [:kind, :brand],
    cracker: :kind
  }
end

salami_1 = Salami.create!
salami_2 = Salami.create!
salami_3 = Salami.create!

limburger = Cheese.create!(kind: "Limburger")
brie = Cheese.create!(kind: "Brie")
pepper_jack = Cheese.create!(kind: "Pepper Jack")

Cracker.create!(kind: "Black Pepper", cheeses: [brie], salami: salami_1)
Cracker.create!(kind: "Ritz", cheeses: [limburger, pepper_jack], salami: salami_2)
Cracker.create!(kind: "Graham", cheeses: [limburger], salami: salami_3)

Salami.tasty_search("pepper") # => [salami_1, salami_2]

Searching using different search features

By default, pg_search_scope uses the built-in PostgreSQL text search. If you pass the :using option to pg_search_scope, you can choose alternative search techniques.

class Beer < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :search_name, against: :name, using: [:tsearch, :trigram, :dmetaphone]
end

Here's an example if you pass multiple :using options with additional configurations.

class Beer < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :search_name, 
  against: :name, 
  using: {
      :trigram => {},
      :dmetaphone => {},
      :tsearch => { :prefix => true }
  }
end

The currently implemented features are

:tsearch (Full Text Search)

PostgreSQL's built-in full text search supports weighting, prefix searches, and stemming in multiple languages.

Weighting

Each searchable column can be given a weight of "A", "B", "C", or "D". Columns with earlier letters are weighted higher than those with later letters. So, in the following example, the title is the most important, followed by the subtitle, and finally the content.

class NewsArticle < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :search_full_text, against: {
    title: 'A',
    subtitle: 'B',
    content: 'C'
  }
end

You can also pass the weights in as an array of arrays, or any other structure that responds to #each and yields either a single symbol or a symbol and a weight. If you omit the weight, a default will be used.

class NewsArticle < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :search_full_text, against: [
    [:title, 'A'],
    [:subtitle, 'B'],
    [:content, 'C']
  ]
end

class NewsArticle < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :search_full_text, against: [
    [:title, 'A'],
    {subtitle: 'B'},
    :content
  ]
end

:prefix (PostgreSQL 8.4 and newer only)

PostgreSQL's full text search matches on whole words by default. If you want to search for partial words, however, you can set :prefix to true. Since this is a :tsearch-specific option, you should pass it to :tsearch directly, as shown in the following example.

class Superhero < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :whose_name_starts_with,
                  against: :name,
                  using: {
                    tsearch: { prefix: true }
                  }
end

batman = Superhero.create name: 'Batman'
batgirl = Superhero.create name: 'Batgirl'
robin = Superhero.create name: 'Robin'

Superhero.whose_name_starts_with("Bat") # => [batman, batgirl]

:negation

PostgreSQL's full text search matches all search terms by default. If you want to exclude certain words, you can set :negation to true. Then any term that begins with an exclamation point ! will be excluded from the results. Since this is a :tsearch-specific option, you should pass it to :tsearch directly, as shown in the following example.

Note that combining this with other search features can have unexpected results. For example, :trigram searches don't have a concept of excluded terms, and thus if you use both :tsearch and :trigram in tandem, you may still find results that contain the term that you were trying to exclude.

class Animal < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :with_name_matching,
                  against: :name,
                  using: {
                    tsearch: {negation: true}
                  }
end

one_fish = Animal.create(name: "one fish")
two_fish = Animal.create(name: "two fish")
red_fish = Animal.create(name: "red fish")
blue_fish = Animal.create(name: "blue fish")

Animal.with_name_matching("fish !red !blue") # => [one_fish, two_fish]

:dictionary

PostgreSQL full text search also support multiple dictionaries for stemming. You can learn more about how dictionaries work by reading the PostgreSQL documention. If you use one of the language dictionaries, such as "english", then variants of words (e.g. "jumping" and "jumped") will match each other. If you don't want stemming, you should pick the "simple" dictionary which does not do any stemming. If you don't specify a dictionary, the "simple" dictionary will be used.

class BoringTweet < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :kinda_matching,
                  against: :text,
                  using: {
                    tsearch: {dictionary: "english"}
                  }
  pg_search_scope :literally_matching,
                  against: :text,
                  using: {
                    tsearch: {dictionary: "simple"}
                  }
end

sleep = BoringTweet.create! text: "I snoozed my alarm for fourteen hours today. I bet I can beat that tomorrow! #sleep"
sleeping = BoringTweet.create! text: "You know what I like? Sleeping. That's what. #enjoyment"
sleeps = BoringTweet.create! text: "In the jungle, the mighty jungle, the lion sleeps #tonight"

BoringTweet.kinda_matching("sleeping") # => [sleep, sleeping, sleeps]
BoringTweet.literally_matching("sleeps") # => [sleeps]

:normalization

PostgreSQL supports multiple algorithms for ranking results against queries. For instance, you might want to consider overall document size or the distance between multiple search terms in the original text. This option takes an integer, which is passed directly to PostgreSQL. According to the latest PostgreSQL documentation, the supported algorithms are:

0 (the default) ignores the document length
1 divides the rank by 1 + the logarithm of the document length
2 divides the rank by the document length
4 divides the rank by the mean harmonic distance between extents
8 divides the rank by the number of unique words in document
16 divides the rank by 1 + the logarithm of the number of unique words in document
32 divides the rank by itself + 1

This integer is a bitmask, so if you want to combine algorithms, you can add their numbers together. (e.g. to use algorithms 1, 8, and 32, you would pass 1 + 8 + 32 = 41)

class BigLongDocument < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :regular_search,
                  against: :text

  pg_search_scope :short_search,
                  against: :text,
                  using: {
                    tsearch: {normalization: 2}
                  }

long = BigLongDocument.create!(text: "Four score and twenty years ago")
short = BigLongDocument.create!(text: "Four score")

BigLongDocument.regular_search("four score") #=> [long, short]
BigLongDocument.short_search("four score") #=> [short, long]

:any_word

Setting this attribute to true will perform a search which will return all models containing any word in the search terms.

class Number < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :search_any_word,
                  against: :text,
                  using: {
                    tsearch: {any_word: true}
                  }

  pg_search_scope :search_all_words,
                  against: :text
end

one = Number.create! text: 'one'
two = Number.create! text: 'two'
three = Number.create! text: 'three'

Number.search_any_word('one two three') # => [one, two, three]
Number.search_all_words('one two three') # => []

:sort_only

Setting this attribute to true will make this feature available for sorting, but will not include it in the query's WHERE condition.

class Person < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :search,
                  against: :name,
                  using: {
                    tsearch: {any_word: true},
                    dmetaphone: {any_word: true, sort_only: true}
                  }
end

exact = Person.create!(name: 'ash hines')
one_exact_one_close = Person.create!(name: 'ash heinz')
one_exact = Person.create!(name: 'ash smith')
one_close = Person.create!(name: 'leigh heinz')

Person.search('ash hines') # => [exact, one_exact_one_close, one_exact]

:highlight

Adding .with_pg_search_highlight after the pg_search_scope you can access to pg_highlight attribute for each object.

class Person < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :search,
                  against: :bio,
                  using: {
                    tsearch: {
                      highlight: {
                        StartSel: '<b>',
                        StopSel: '</b>',
                        MaxWords: 123,
                        MinWords: 456,
                        ShortWord: 4,
                        HighlightAll: true,
                        MaxFragments: 3,
                        FragmentDelimiter: '&hellip;'
                      }
                    }
                  }
end

Person.create!(:bio => "Born in rural Alberta, where the buffalo roam.")

first_match = Person.search("Alberta").with_pg_search_highlight.first
first_match.pg_search_highlight # => "Born in rural <b>Alberta</b>, where the buffalo roam."

The highlight option accepts all options supported by ts_headline, and uses PostgreSQL's defaults.

See the documentation for details on the meaning of each option.

:dmetaphone (Double Metaphone soundalike search)

Double Metaphone is an algorithm for matching words that sound alike even if they are spelled very differently. For example, "Geoff" and "Jeff" sound identical and thus match. Currently, this is not a true double-metaphone, as only the first metaphone is used for searching.

Double Metaphone support is currently available as part of the fuzzystrmatch extension that must be installed before this feature can be used. In addition to the extension, you must install a utility function into your database. To generate and run a migration for this, run:

$ rails g pg_search:migration:dmetaphone
$ rake db:migrate

The following example shows how to use :dmetaphone.

class Word < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :that_sounds_like,
                  against: :spelling,
                  using: :dmetaphone
end

four = Word.create! spelling: 'four'
far = Word.create! spelling: 'far'
fur = Word.create! spelling: 'fur'
five = Word.create! spelling: 'five'

Word.that_sounds_like("fir") # => [four, far, fur]

:trigram (Trigram search)

Trigram search works by counting how many three-letter substrings (or "trigrams") match between the query and the text. For example, the string "Lorem ipsum" can be split into the following trigrams:

[" Lo", "Lor", "ore", "rem", "em ", "m i", " ip", "ips", "psu", "sum", "um ", "m  "]

Trigram search has some ability to work even with typos and misspellings in the query or text.

Trigram support is currently available as part of the pg_trgm extension that must be installed before this feature can be used.

class Website < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :kinda_spelled_like,
                  against: :name,
                  using: :trigram
end

yahooo = Website.create! name: "Yahooo!"
yohoo = Website.create! name: "Yohoo!"
gogle = Website.create! name: "Gogle"
facebook = Website.create! name: "Facebook"

Website.kinda_spelled_like("Yahoo!") # => [yahooo, yohoo]

:threshold

By default, trigram searches find records which have a similarity of at least 0.3 using pg_trgm's calculations. You may specify a custom threshold if you prefer. Higher numbers match more strictly, and thus return fewer results. Lower numbers match more permissively, letting in more results. Please note that setting a trigram threshold will force a table scan as the derived query uses the similarity() function instead of the % operator.

class Vegetable < ActiveRecord::Base
  include PgSearch::Model

  pg_search_scope :strictly_spelled_like,
                  against: :name,
                  using: {
                    trigram: {
                      threshold: 0.5
                    }
                  }

  pg_search_scope :roughly_spelled_like,
                  against: :name,
                  using: {
                    trigram: {
                      threshold: 0.1
                    }
                  }
end

cauliflower = Vegetable.create! name: "cauliflower"

Vegetable.roughly_spelled_like("couliflower") # => [cauliflower]
Vegetable.strictly_spelled_like("couliflower") # => [cauliflower]

Vegetable.roughly_spelled_like("collyflower") # => [cauliflower]
Vegetable.strictly_spelled_like("collyflower") # => []

:word_similarity

Allows you to match words in longer strings. By default, trigram searches use % or similarity() as a similarity value. Set word_similarity to true to opt for <% and word_similarity instead. This causes the trigram search to use the similarity of the query term and the word with greatest similarity.

class Sentence < ActiveRecord::Base
  include PgSearch::Model

  pg_search_scope :similarity_like,
                  against: :name,
                  using: {
                    trigram: {
                      word_similarity: true
                    }
                  }

  pg_search_scope :word_similarity_like,
                  against: :name,
                  using: [:trigram]
end

sentence = Sentence.create! name: "Those are two words."

Sentence.similarity_like("word") # => []
Sentence.word_similarity_like("word") # => [sentence]

Limiting Fields When Combining Features

Sometimes when doing queries combining different features you might want to searching against only some of the fields with certain features. For example perhaps you want to only do a trigram search against the shorter fields so that you don't need to reduce the threshold excessively. You can specify which fields using the 'only' option:

class Image < ActiveRecord::Base
  include PgSearch::Model

  pg_search_scope :combined_search,
                  against: [:file_name, :short_description, :long_description]
                  using: {
                    tsearch: { dictionary: 'english' },
                    trigram: {
                      only: [:file_name, :short_description]
                    }
                  }

end

Now you can succesfully retrieve an Image with a file_name: 'image_foo.jpg' and long_description: 'This description is so long that it would make a trigram search fail any reasonable threshold limit' with:

Image.combined_search('reasonable') # found with tsearch
Image.combined_search('foo') # found with trigram

Ignoring accent marks

Most of the time you will want to ignore accent marks when searching. This makes it possible to find words like "piñata" when searching with the query "pinata". If you set a pg_search_scope to ignore accents, it will ignore accents in both the searchable text and the query terms.

Ignoring accents uses the unaccent extension that must be installed before this feature can be used.

class SpanishQuestion < ActiveRecord::Base
  include PgSearch::Model
  pg_search_scope :gringo_search,
                  against: :word,
                  ignoring: :accents
end

what = SpanishQuestion.create(word: "Qué")
how_many = SpanishQuestion.create(word: "Cuánto")
how = SpanishQuestion.create(word: "Cómo")

SpanishQuestion.gringo_search("Que") # => [what]
SpanishQuestion.gringo_search("Cüåñtô") # => [how_many]

Advanced users may wish to add indexes for the expressions that pg_search generates. Unfortunately, the unaccent function supplied by this extension is not indexable (as of PostgreSQL 9.1). Thus, you may want to write your own wrapper function and use it instead. This can be configured by calling the following code, perhaps in an initializer.

PgSearch.unaccent_function = "my_unaccent"

Using tsvector columns

PostgreSQL allows you the ability to search against a column with type tsvector instead of using an expression; this speeds up searching dramatically as it offloads creation of the tsvector that the tsquery is evaluated against.

To use this functionality you'll need to do a few things:

Create a column of type tsvector that you'd like to search against. If you want to search using multiple search methods, for example tsearch and dmetaphone, you'll need a column for each.

Create a trigger function that will update the column(s) using the expression appropriate for that type of search. See: the PostgreSQL documentation for text search triggers

Should you have any pre-existing data in the table, update the newly-created tsvector columns with the expression that your trigger function uses.

Add the option to pg_search_scope, e.g:

pg_search_scope :fast_content_search,
                against: :content,
                using: {
                  dmetaphone: {
                    tsvector_column: 'tsvector_content_dmetaphone'
                  },
                  tsearch: {
                    dictionary: 'english',
                    tsvector_column: 'tsvector_content_tsearch'
                  },
                  trigram: {} # trigram does not use tsvectors
                }

Please note that the :against column is only used when the tsvector_column is not present for the search type.

Combining multiple tsvectors

It's possible to search against more than one tsvector at a time. This could be useful if you want to maintain multiple search scopes but do not want to maintain separate tsvectors for each scope. For example:

pg_search_scope :search_title,
                against: :title,
                using: {
                  tsearch: {
                    tsvector_column: "title_tsvector"
                  }
                }

pg_search_scope :search_body,
                against: :body,
                using: {
                  tsearch: {
                    tsvector_column: "body_tsvector"
                  }
                }

pg_search_scope :search_title_and_body,
                against: [:title, :body],
                using: {
                  tsearch: {
                    tsvector_column: ["title_tsvector", "body_tsvector"]
                  }
                }

Configuring ranking and ordering

:ranked_by (Choosing a ranking algorithm)

By default, pg_search ranks results based on the :tsearch similarity between the searchable text and the query. To use a different ranking algorithm, you can pass a :ranked_by option to pg_search_scope.

pg_search_scope :search_by_tsearch_but_rank_by_trigram,
                against: :title,
                using: [:tsearch],
                ranked_by: ":trigram"

Note that :ranked_by using a String to represent the ranking expression. This allows for more complex possibilities. Strings like ":tsearch", ":trigram", and ":dmetaphone" are automatically expanded into the appropriate SQL expressions.

# Weighted ranking to balance multiple approaches
ranked_by: ":dmetaphone + (0.25 * :trigram)"

# A more complex example, where books.num_pages is an integer column in the table itself
ranked_by: "(books.num_pages * :trigram) + (:tsearch / 2.0)"

:order_within_rank (Breaking ties)

PostgreSQL does not guarantee a consistent order when multiple records have the same value in the ORDER BY clause. This can cause trouble with pagination. Imagine a case where 12 records all have the same ranking value. If you use a pagination library such as kaminari or will_paginate to return results in pages of 10, then you would expect to see 10 of the records on page 1, and the remaining 2 records at the top of the next page, ahead of lower-ranked results.

But since there is no consistent ordering, PostgreSQL might choose to rearrange the order of those 12 records between different SQL statements. You might end up getting some of the same records from page 1 on page 2 as well, and likewise there may be records that don't show up at all.

pg_search fixes this problem by adding a second expression to the ORDER BY clause, after the :ranked_by expression explained above. By default, the tiebreaker order is ascending by id.

ORDER BY [complicated :ranked_by expression...], id ASC

This might not be desirable for your application, especially if you do not want old records to outrank new records. By passing an :order_within_rank, you can specify an alternate tiebreaker expression. A common example would be descending by updated_at, to rank the most recently updated records first.

pg_search_scope :search_and_break_ties_by_latest_update,
                against: [:title, :content],
                order_within_rank: "blog_posts.updated_at DESC"

PgSearch#pg_search_rank (Reading a record's rank as a Float)

It may be useful or interesting to see the rank of a particular record. This can be helpful for debugging why one record outranks another. You could also use it to show some sort of relevancy value to end users of an application.

To retrieve the rank, call .with_pg_search_rank on a scope, and then call .pg_search_rank on a returned record.

shirt_brands = ShirtBrand.search_by_name("Penguin").with_pg_search_rank
shirt_brands[0].pg_search_rank #=> 0.0759909
shirt_brands[1].pg_search_rank #=> 0.0607927

Search rank and chained scopes

Each PgSearch scope generates a named subquery for the search rank. If you chain multiple scopes then PgSearch will generate a ranking query for each scope, so the ranking queries must have unique names. If you need to reference the ranking query (e.g. in a GROUP BY clause) you can regenerate the subquery name with the PgScope::Configuration.alias method by passing the name of the queried table.

shirt_brands = ShirtBrand.search_by_name("Penguin")
  .joins(:shirt_sizes)
  .group("shirt_brands.id, #{PgSearch::Configuration.alias('shirt_brands')}.rank")

ATTRIBUTIONS

PgSearch would not have been possible without inspiration from texticle (now renamed textacular). Thanks to Aaron Patterson for the original version and to Casebook PBC (https://www.casebook.net) for gifting the community with it!

CONTRIBUTIONS AND FEEDBACK

Please read our CONTRIBUTING guide.

We also have a Google Group for discussing pg_search and other Casebook PBC open source projects.


Author: Casecommons
Source code: https://github.com/Casecommons/pg_search
License: MIT license

#ruby #postgresql 

Builds ActiveRecord Named Scopes That Take Advantage Of PostgreSQL's
Beth  Cooper

Beth Cooper

1659753600

Easily Create A Hierarchy That Supports Your ActiveRecord Model

Closure Tree

Closure_tree lets your ActiveRecord models act as nodes in a tree data structure

Common applications include modeling hierarchical data, like tags, threaded comments, page graphs in CMSes, and tracking user referrals.

Dramatically more performant than ancestry and acts_as_tree, and even more awesome than awesome_nested_set, closure_tree has some great features:

  • Best-in-class select performance:
    • Fetch your whole ancestor lineage in 1 SELECT.
    • Grab all your descendants in 1 SELECT.
    • Get all your siblings in 1 SELECT.
    • Fetch all descendants as a nested hash in 1 SELECT.
    • Find a node by ancestry path in 1 SELECT.
  • Best-in-class mutation performance:
    • 2 SQL INSERTs on node creation
    • 3 SQL INSERT/UPDATEs on node reparenting
  • Support for concurrency (using with_advisory_lock)
  • Tested against ActiveRecord 6.0+ with Ruby 2.7+
  • Support for reparenting children (and all their descendants)
  • Support for single-table inheritance (STI) within the hierarchy
  • find_or_create_by_path for building out heterogeneous hierarchies quickly and conveniently
  • Support for deterministic ordering
  • Support for preordered traversal of descendants
  • Support for rendering trees in DOT format, using Graphviz
  • Excellent test coverage in a comprehensive variety of environments

See Bill Karwin's excellent Models for hierarchical data presentation for a description of different tree storage algorithms.

Installation

Note that closure_tree only supports ActiveRecord 6.0 and later, and has test coverage for MySQL, PostgreSQL, and SQLite.

Add gem 'closure_tree' to your Gemfile

Run bundle install

Add has_closure_tree (or acts_as_tree, which is an alias of the same method) to your hierarchical model:

class Tag < ActiveRecord::Base
  has_closure_tree
end

class AnotherTag < ActiveRecord::Base
  acts_as_tree
end

Make sure you check out the large number of options that has_closure_tree accepts.

IMPORTANT: Make sure you add has_closure_tree after attr_accessible and self.table_name = lines in your model.

If you're already using other hierarchical gems, like ancestry or acts_as_tree, please refer to the warning section!

Add a migration to add a parent_id column to the hierarchical model. You may want to also add a column for deterministic ordering of children, but that's optional.

class AddParentIdToTag < ActiveRecord::Migration
  def change
    add_column :tags, :parent_id, :integer
  end
end

The column must be nullable. Root nodes have a NULL parent_id.

Run rails g closure_tree:migration tag (and replace tag with your model name) to create the closure tree table for your model.

By default the table name will be the model's table name, followed by "_hierarchies". Note that by calling has_closure_tree, a "virtual model" (in this case, TagHierarchy) will be created dynamically. You don't need to create it.

Run rake db:migrate

If you're migrating from another system where your model already has a parent_id column, run Tag.rebuild! and your tag_hierarchies table will be truncated and rebuilt.

If you're starting from scratch you don't need to call rebuild!.

NOTE: Run rails g closure_tree:config to create an initializer with extra configurations. (Optional)

Warning

As stated above, using multiple hierarchy gems (like ancestry or nested set) on the same model will most likely result in pain, suffering, hair loss, tooth decay, heel-related ailments, and gingivitis. Assume things will break.

Usage

Creation

Create a root node:

grandparent = Tag.create(name: 'Grandparent')

Child nodes are created by appending to the children collection:

parent = grandparent.children.create(name: 'Parent')

Or by appending to the children collection:

child2 = Tag.new(name: 'Second Child')
parent.children << child2

Or by calling the "add_child" method:

child3 = Tag.new(name: 'Third Child')
parent.add_child child3

Or by setting the parent on the child :

Tag.create(name: 'Fourth Child', parent: parent)

Then:

grandparent.self_and_descendants.collect(&:name)
=> ["Grandparent", "Parent", "First Child", "Second Child", "Third Child", "Fourth Child"]

child1.ancestry_path
=> ["Grandparent", "Parent", "First Child"]

find_or_create_by_path

You can find as well as find_or_create by "ancestry paths".

If you provide an array of strings to these methods, they reference the name column in your model, which can be overridden with the :name_column option provided to has_closure_tree.

child = Tag.find_or_create_by_path(%w[grandparent parent child])

As of v5.0.0, find_or_create_by_path can also take an array of attribute hashes:

child = Tag.find_or_create_by_path([
  {name: 'Grandparent', title: 'Sr.'},
  {name: 'Parent', title: 'Mrs.'},
  {name: 'Child', title: 'Jr.'}
])

If you're using STI, The attribute hashes can contain the sti_name and things work as expected:

child = Label.find_or_create_by_path([
  {type: 'DateLabel', name: '2014'},
  {type: 'DateLabel', name: 'August'},
  {type: 'DateLabel', name: '5'},
  {type: 'EventLabel', name: 'Visit the Getty Center'}
])

Moving nodes around the tree

Nodes can be moved around to other parents, and closure_tree moves the node's descendancy to the new parent for you:

d = Tag.find_or_create_by_path %w[a b c d]
h = Tag.find_or_create_by_path %w[e f g h]
e = h.root
d.add_child(e) # "d.children << e" would work too, of course
h.ancestry_path
=> ["a", "b", "c", "d", "e", "f", "g", "h"]

When it is more convenient to simply change the parent_id of a node directly (for example, when dealing with a form <select>), closure_tree will handle the necessary changes automatically when the record is saved:

j = Tag.find 102
j.self_and_ancestor_ids
=> [102, 87, 77]
j.update parent_id: 96
j.self_and_ancestor_ids
=> [102, 96, 95, 78]

Nested hashes

hash_tree provides a method for rendering a subtree as an ordered nested hash:

b = Tag.find_or_create_by_path %w(a b)
a = b.parent
b2 = Tag.find_or_create_by_path %w(a b2)
d1 = b.find_or_create_by_path %w(c1 d1)
c1 = d1.parent
d2 = b.find_or_create_by_path %w(c2 d2)
c2 = d2.parent

Tag.hash_tree
=> {a => {b => {c1 => {d1 => {}}, c2 => {d2 => {}}}, b2 => {}}}

Tag.hash_tree(:limit_depth => 2)
=> {a => {b => {}, b2 => {}}}

b.hash_tree
=> {b => {c1 => {d1 => {}}, c2 => {d2 => {}}}}

b.hash_tree(:limit_depth => 2)
=> {b => {c1 => {}, c2 => {}}}

If your tree is large (or might become so), use :limit_depth.

Without this option, hash_tree will load the entire contents of that table into RAM. Your server may not be happy trying to do this.

HT: ancestry and elhoyos

Eager loading

Since most of closure_tree's methods (e.g. children) return regular ActiveRecord scopes, you can use the includes method for eager loading, e.g.

comment.children.includes(:author)

However, note that the above approach only eager loads the requested associations for the immediate children of comment. If you want to walk through the entire tree, you may still end up making many queries and loading duplicate copies of objects.

In some cases, a viable alternative is the following:

comment.self_and_descendants.includes(:author)

This would load authors for comment and all its descendants in a constant number of queries. However, the return value is an array of Comments, and the tree structure is thus lost, which makes it difficult to walk the tree using elegant recursive algorithms.

A third option is to use has_closure_tree_root on the model that is composed by the closure_tree model (e.g. a Post may be composed by a tree of Comments). So in post.rb, you would do:

# app/models/post.rb
has_closure_tree_root :root_comment

This gives you a plain has_one association (root_comment) to the root Comment (i.e. that with null parent_id).

It also gives you a method called root_comment_including_tree, which you can invoke as follows:

a_post.root_comment_including_tree(:author)

The result of this call will be the root Comment with all descendants and associations loaded in a constant number of queries. Inverse associations are also setup on all nodes, so as you walk the tree, calling children or parent on any node will not trigger any further queries and no duplicate copies of objects are loaded into memory.

The class and foreign key of root_comment are assumed to be Comment and post_id, respectively. These can be overridden in the usual way.

The same caveat stated above with hash_tree also applies here: this method will load the entire tree into memory. If the tree is very large, this may be a bad idea, in which case using the eager loading methods above may be preferred.

Graph visualization

to_dot_digraph is suitable for passing into Graphviz.

For example, for the above tree, write out the DOT file with ruby:

File.open("example.dot", "w") { |f| f.write(Tag.root.to_dot_digraph) }

Then, in a shell, dot -Tpng example.dot > example.png, which produces:

Example tree

If you want to customize the label value, override the #to_digraph_label instance method in your model.

Just for kicks, this is the test tree I used for proving that preordered tree traversal was correct:

Preordered test tree

Available options

When you include has_closure_tree in your model, you can provide a hash to override the following defaults:

  • :parent_column_name to override the column name of the parent foreign key in the model's table. This defaults to "parent_id".
  • :hierarchy_class_name to override the hierarchy class name. This defaults to the singular name of the model + "Hierarchy", like TagHierarchy.
  • :hierarchy_table_name to override the hierarchy table name. This defaults to the singular name of the model + "_hierarchies", like tag_hierarchies.
  • :dependent determines what happens when a node is destroyed. Defaults to nullify.
    • :nullify will simply set the parent column to null. Each child node will be considered a "root" node. This is the default.
    • :delete_all will delete all descendant nodes (which circumvents the destroy hooks)
    • :destroy will destroy all descendant nodes (which runs the destroy hooks on each child node)
    • nil does nothing with descendant nodes
  • :name_column used by #find_or_create_by_path, #find_by_path, and ancestry_path instance methods. This is primarily useful if the model only has one required field (like a "tag").
  • :order used to set up deterministic ordering
  • :touch delegates to the belongs_to annotation for the parent, so touching cascades to all children (the performance of this for deep trees isn't currently optimal).

Accessing Data

Class methods

Tag.root returns an arbitrary root node

Tag.roots returns all root nodes

Tag.leaves returns all leaf nodes

Tag.hash_tree returns an ordered, nested hash that can be depth-limited.

Tag.find_by_path(path, attributes) returns the node whose name path is path. See (#find_or_create_by_path).

Tag.find_or_create_by_path(path, attributes) returns the node whose name path is path, and will create the node if it doesn't exist already.See (#find_or_create_by_path).

Tag.find_all_by_generation(generation_level) returns the descendant nodes who are generation_level away from a root. Tag.find_all_by_generation(0) is equivalent to Tag.roots.

Tag.with_ancestor(ancestors) scopes to all descendants whose ancestors(s) is/are in the given list.

Tag.with_descendant(ancestors) scopes to all ancestors whose descendant(s) is/are in the given list.

Tag.lowest_common_ancestor(descendants) finds the lowest common ancestor of the descendants.

Instance methods

tag.root returns the root for this node

tag.root? returns true if this is a root node

tag.root_of?(node) returns true if current node is root of another one

tag.child? returns true if this is a child node. It has a parent.

tag.leaf? returns true if this is a leaf node. It has no children.

tag.leaves is scoped to all leaf nodes in self_and_descendants.

tag.depth returns the depth, or "generation", for this node in the tree. A root node will have a value of 0.

tag.parent returns the node's immediate parent. Root nodes will return nil.

tag.parent_of?(node) returns true if current node is parent of another one

tag.children is a has_many of immediate children (just those nodes whose parent is the current node).

tag.child_ids is an array of the IDs of the children.

tag.child_of?(node) returns true if current node is child of another one

tag.ancestors is a ordered scope of [ parent, grandparent, great grandparent, … ]. Note that the size of this array will always equal tag.depth.

tag.ancestor_ids is an array of the IDs of the ancestors.

tag.ancestor_of?(node) returns true if current node is ancestor of another one

tag.self_and_ancestors returns a scope containing self, parent, grandparent, great grandparent, etc.

tag.self_and_ancestors_ids returns IDs containing self, parent, grandparent, great grandparent, etc.

tag.siblings returns a scope containing all nodes with the same parent as tag, excluding self.

tag.sibling_ids returns an array of the IDs of the siblings.

tag.self_and_siblings returns a scope containing all nodes with the same parent as tag, including self.

tag.descendants returns a scope of all children, childrens' children, etc., excluding self ordered by depth.

tag.descendant_ids returns an array of the IDs of the descendants.

tag.descendant_of?(node) returns true if current node is descendant of another one

tag.self_and_descendants returns a scope of self, all children, childrens' children, etc., ordered by depth.

tag.self_and_descendant_ids returns IDs of self, all children, childrens' children, etc., ordered by depth.

tag.family_of? returns true if current node and another one have a same root.

tag.hash_tree returns an ordered, nested hash that can be depth-limited.

tag.find_by_path(path) returns the node whose name path from tag is path. See (#find_or_create_by_path).

tag.find_or_create_by_path(path) returns the node whose name path from tag is path, and will create the node if it doesn't exist already.See (#find_or_create_by_path).

tag.find_all_by_generation(generation_level) returns the descendant nodes who are generation_level away from tag.

  • tag.find_all_by_generation(0).to_a == [tag]
  • tag.find_all_by_generation(1) == tag.children
  • tag.find_all_by_generation(2) will return the tag's grandchildren, and so on.

tag.destroy will destroy a node and do something to its children, which is determined by the :dependent option passed to has_closure_tree.

Polymorphic hierarchies with STI

Polymorphic models using single table inheritance (STI) are supported:

  1. Create a db migration that adds a String type column to your model
  2. Subclass the model class. You only need to add has_closure_tree to your base class:
class Tag < ActiveRecord::Base
  has_closure_tree
end
class WhenTag < Tag ; end
class WhereTag < Tag ; end
class WhatTag < Tag ; end

Note that if you call rebuild! on any of the subclasses, the complete Tag hierarchy will be emptied, thus taking the hiearchies of all other subclasses with it (issue #275). However, only the hierarchies for the class rebuild! was called on will be rebuilt, leaving the other subclasses without hierarchy entries.

You can work around that by overloading the rebuild! class method in all your STI subclasses and call the super classes rebuild! method:

class WhatTag < Tag
  def self.rebuild!
    Tag.rebuild!
  end
end

This way, the complete hierarchy including all subclasses will be rebuilt.

Deterministic ordering

By default, children will be ordered by your database engine, which may not be what you want.

If you want to order children alphabetically, and your model has a name column, you'd do this:

class Tag < ActiveRecord::Base
  has_closure_tree order: 'name'
end

If you want a specific order, add a new integer column to your model in a migration:

t.integer :sort_order

and in your model:

class OrderedTag < ActiveRecord::Base
  has_closure_tree order: 'sort_order', numeric_order: true
end

When you enable order, you'll also have the following new methods injected into your model:

  • tag.siblings_before is a scope containing all nodes with the same parent as tag, whose sort order column is less than self. These will be ordered properly, so the last element in scope will be the sibling immediately before self
  • tag.siblings_after is a scope containing all nodes with the same parent as tag, whose sort order column is more than self. These will be ordered properly, so the first element in scope will be the sibling immediately "after" self

If your order column is an integer attribute, you'll also have these:

The class method #roots_and_descendants_preordered, which returns all nodes in your tree, pre-ordered.

node1.self_and_descendants_preordered which will return descendants, pre-ordered.

node1.append_child(node2) (which is an alias to add_child), which will

  1. set node2's parent to node1
  2. set node2's sort order to place node2 last in the children array

node1.prepend_child(node2) which will

  1. set node2's parent to node1
  2. set node2's sort order to place node2 first in the children array Note that all of node1's children's sort_orders will be incremented

node1.prepend_sibling(node2) which will

  1. set node2 to the same parent as node1,
  2. set node2's order column to 1 less than node1's value, and
  3. increment the order_column of all children of node1's parents whose order_column is > node2's new value by 1.

node1.append_sibling(node2) which will

  1. set node2 to the same parent as node1,
  2. set node2's order column to 1 more than node1's value, and
  3. increment the order_column of all children of node1's parents whose order_column is > node2's new value by 1.

root = OrderedTag.create(name: 'root')
a = root.append_child(Label.new(name: 'a'))
b = OrderedTag.create(name: 'b')
c = OrderedTag.create(name: 'c')

# We have to call 'root.reload.children' because root won't be in sync with the database otherwise:

a.append_sibling(b)
root.reload.children.pluck(:name)
=> ["a", "b"]

a.prepend_sibling(b)
root.reload.children.pluck(:name)
=> ["b", "a"]

a.append_sibling(c)
root.reload.children.pluck(:name)
=> ["b", "a", "c"]

b.append_sibling(c)
root.reload.children.pluck(:name)
=> ["b", "c", "a"]

Ordering Roots

With numeric ordering, root nodes are, by default, assigned order values globally across the whole database table. So for instance if you have 5 nodes with no parent, they will be ordered 0 through 4 by default. If your model represents many separate trees and you have a lot of records, this can cause performance problems, and doesn't really make much sense.

You can disable this default behavior by passing dont_order_roots: true as an option to your delcaration:

has_closure_tree order: 'sort_order', numeric_order: true, dont_order_roots: true

In this case, calling prepend_sibling and append_sibling on a root node or calling roots_and_descendants_preordered on the model will raise a RootOrderingDisabledError.

The dont_order_roots option will be ignored unless numeric_order is set to true.

Concurrency

Several methods, especially #rebuild and #find_or_create_by_path, cannot run concurrently correctly. #find_or_create_by_path, for example, may create duplicate nodes.

Database row-level locks work correctly with PostgreSQL, but MySQL's row-level locking is broken, and erroneously reports deadlocks where there are none. To work around this, and have a consistent implementation for both MySQL and PostgreSQL, with_advisory_lock is used automatically to ensure correctness.

If you are already managing concurrency elsewhere in your application, and want to disable the use of with_advisory_lock, pass with_advisory_lock: false in the options hash:

class Tag
  has_closure_tree with_advisory_lock: false
end

Note that you will eventually have data corruption if you disable advisory locks, write to your database with multiple threads, and don't provide an alternative mutex.

I18n

You can customize error messages using I18n:

en-US:
  closure_tree:
    loop_error: Your descendant cannot be your parent!

FAQ

Are there any how-to articles on how to use this gem?

Yup! Ilya Bodrov wrote Nested Comments with Rails.

Does this work well with #default_scope?

No. Please see issue 86 for details.

Can I update parentage with update_attribute?

No. update_attribute skips the validation hook that is required for maintaining the hierarchy table.

Can I assign a parent to multiple children with #update_all?

No. Please see issue 197 for details.

Does this gem support multiple parents?

No. This gem's API is based on the assumption that each node has either 0 or 1 parent.

The underlying closure tree structure will support multiple parents, but there would be many breaking-API changes to support it. I'm open to suggestions and pull requests.

How do I use this with test fixtures?

Test fixtures aren't going to be running your after_save hooks after inserting all your fixture data, so you need to call .rebuild! before your test runs. There's an example in the spec tag_spec.rb:

  describe "Tag with fixtures" do
    fixtures :tags
    before :each do
      Tag.rebuild! # <- required if you use fixtures
    end

However, if you're just starting with Rails, may I humbly suggest you adopt a factory library, rather than using fixtures? Lots of people have written about this already.

There are many lock-* files in my project directory after test runs

This is expected if you aren't using MySQL or Postgresql for your tests.

SQLite doesn't have advisory locks, so we resort to file locking, which will only work if the FLOCK_DIR is set consistently for all ruby processes.

In your spec_helper.rb or minitest_helper.rb, add a before and after block:

before do
  ENV['FLOCK_DIR'] = Dir.mktmpdir
end

after do
  FileUtils.remove_entry_secure ENV['FLOCK_DIR']
end

bundle install says Gem::Ext::BuildError: ERROR: Failed to build gem native extension

When building from source, the mysql2, pg, and sqlite gems need their native client libraries installed on your system. Note that this error isn't specific to ClosureTree.

On Ubuntu/Debian systems, run:

sudo apt-get install libpq-dev libsqlite3-dev libmysqlclient-dev
bundle install

Object destroy fails with MySQL v5.7+

A bug was introduced in MySQL's query optimizer. See the workaround here.

Hierarchy maintenance errors from MySQL v5.7.9-v5.7.10

Upgrade to MySQL 5.7.12 or later if you see this issue:

Mysql2::Error: You can't specify target table '*_hierarchies' for update in FROM clause

Testing with Closure Tree

Closure tree comes with some RSpec2/3 matchers which you may use for your tests:

require 'spec_helper'
require 'closure_tree/test/matcher'

describe Category do
 # Should syntax
 it { should be_a_closure_tree }
 # Expect syntax
 it { is_expected.to be_a_closure_tree }
end

describe Label do
 # Should syntax
 it { should be_a_closure_tree.ordered }
 # Expect syntax
 it { is_expected.to be_a_closure_tree.ordered }
end

describe TodoList::Item do
 # Should syntax
 it { should be_a_closure_tree.ordered(:priority_order) }
 # Expect syntax
 it { is_expected.to be_a_closure_tree.ordered(:priority_order) }
end

Testing

Closure tree is tested under every valid combination of

  • Ruby 2.7+
  • ActiveRecord 6.0+
  • PostgreSQL, MySQL, and SQLite. Concurrency tests are only run with MySQL and PostgreSQL.

Assuming you're using rbenv, you can use tests.sh to run the test matrix locally.

Change log

See the change log.

Thanks to


Author:  ClosureTree
Source code: https://github.com/ClosureTree/closure_tree
License: MIT license

#ruby   #ruby-on-rails 

Easily Create A Hierarchy That Supports Your ActiveRecord Model

Ruby Persistence Framework with Entities and Repositories

Hanami::Model

A persistence framework for Hanami.

It delivers a convenient public API to execute queries and commands against a database. The architecture eases keeping the business logic (entities) separated from details such as persistence or validations.

It implements the following concepts:

  • Entity - A model domain object defined by its identity.
  • Repository - An object that mediates between the entities and the persistence layer.

Like all the other Hanami components, it can be used as a standalone framework or within a full Hanami application.

Version

This branch contains the code for hanami-model 2.x.

Status

Gem Version CI Test Coverage Depfu Inline Docs

Contact

Rubies

Hanami::Model supports Ruby (MRI) 2.6+

Installation

Add this line to your application's Gemfile:

gem 'hanami-model'

And then execute:

$ bundle

Or install it yourself as:

$ gem install hanami-model

Usage

This class provides a DSL to configure the connection.

require 'hanami/model'
require 'hanami/model/sql'

class User < Hanami::Entity
end

class UserRepository < Hanami::Repository
end

Hanami::Model.configure do
  adapter :sql, 'postgres://username:password@localhost/bookshelf'
end.load!

repository = UserRepository.new
user       = repository.create(name: 'Luca')

puts user.id # => 1

found = repository.find(user.id)
found == user # => true

updated = repository.update(user.id, age: 34)
updated.age # => 34

repository.delete(user.id)

Concepts

Entities

A model domain object that is defined by its identity. See "Domain Driven Design" by Eric Evans.

An entity is the core of an application, where the part of the domain logic is implemented. It's a small, cohesive object that expresses coherent and meaningful behaviors.

It deals with one and only one responsibility that is pertinent to the domain of the application, without caring about details such as persistence or validations.

This simplicity of design allows developers to focus on behaviors, or message passing if you will, which is the quintessence of Object Oriented Programming.

require 'hanami/model'

class Person < Hanami::Entity
end

Repositories

An object that mediates between entities and the persistence layer. It offers a standardized API to query and execute commands on a database.

A repository is storage independent, all the queries and commands are delegated to the current adapter.

This architecture has several advantages:

Applications depend on a standard API, instead of low level details (Dependency Inversion principle)

Applications depend on a stable API, that doesn't change if the storage changes

Developers can postpone storage decisions

Confines persistence logic at a low level

Multiple data sources can easily coexist in an application

When a class inherits from Hanami::Repository, it will receive the following interface:

  • #create(data) – Create a record for the given data (or entity)
  • #update(id, data) – Update the record corresponding to the given id by setting the given data (or entity)
  • #delete(id) – Delete the record corresponding to the given id
  • #all - Fetch all the entities from the relation
  • #find - Fetch an entity from the relation by primary key
  • #first - Fetch the first entity from the relation
  • #last - Fetch the last entity from the relation
  • #clear - Delete all the records from the relation

A relation is a homogenous set of records. It corresponds to a table for a SQL database or to a MongoDB collection.

All the queries are private. This decision forces developers to define intention revealing API, instead of leaking storage API details outside of a repository.

Look at the following code:

ArticleRepository.new.where(author_id: 23).order(:published_at).limit(8)

This is bad for a variety of reasons:

The caller has an intimate knowledge of the internal mechanisms of the Repository.

The caller works on several levels of abstraction.

It doesn't express a clear intent, it's just a chain of methods.

The caller can't be easily tested in isolation.

If we change the storage, we are forced to change the code of the caller(s).

There is a better way:

require 'hanami/model'

class ArticleRepository < Hanami::Repository
  def most_recent_by_author(author, limit: 8)
    articles.where(author_id: author.id).
      order(:published_at).
      limit(limit)
  end
end

This is a huge improvement, because:

The caller doesn't know how the repository fetches the entities.

The caller works on a single level of abstraction. It doesn't even know about records, only works with entities.

It expresses a clear intent.

The caller can be easily tested in isolation. It's just a matter of stubbing this method.

If we change the storage, the callers aren't affected.

Mapping

Hanami::Model can automap columns from relations and entities attributes.

When using a sql adapter, you must require hanami/model/sql before Hanami::Model.load! is called so the relations are loaded correctly.

However, there are cases where columns and attribute names do not match (mainly legacy databases).

require 'hanami/model'

class UserRepository < Hanami::Repository
  self.relation = :t_user_archive

  mapping do
    attribute :id,   from: :i_user_id
    attribute :name, from: :s_name
    attribute :age,  from: :i_age
  end
end

NOTE: This feature should be used only when automapping fails because of the naming mismatch.

Conventions

  • A repository must be named after an entity, by appending "Repository" to the entity class name (eg. Article => ArticleRepository).

Thread safety

Hanami::Model's is thread safe during the runtime, but it isn't during the loading process. The mapper compiles some code internally, so be sure to safely load it before your application starts.

Mutex.new.synchronize do
  Hanami::Model.load!
end

This is not necessary when Hanami::Model is used within a Hanami application.

Features

Timestamps

If an entity has the following accessors: :created_at and :updated_at, they will be automatically updated when the entity is persisted.

require 'hanami/model'
require 'hanami/model/sql'

class User < Hanami::Entity
end

class UserRepository < Hanami::Repository
end

Hanami::Model.configure do
  adapter :sql, 'postgresql://localhost/bookshelf'
end.load!

repository = UserRepository.new

user = repository.create(name: 'Luca')

puts user.created_at.to_s # => "2016-09-19 13:40:13 UTC"
puts user.updated_at.to_s # => "2016-09-19 13:40:13 UTC"

sleep 3
user = repository.update(user.id, age: 34)
puts user.created_at.to_s # => "2016-09-19 13:40:13 UTC"
puts user.updated_at.to_s # => "2016-09-19 13:40:16 UTC"

Configuration

Logging

In order to log database operations, you can configure a logger:

Hanami::Model.configure do
  # ...
  logger "log/development.log", level: :debug
end

It accepts the following arguments:

  • stream: a Ruby StringIO object - it can be $stdout or a path to file (eg. "log/development.log") - Defaults to $stdout
  • :level: logging level - it can be: :debug, :info, :warn, :error, :fatal, :unknown - Defaults to :debug
  • :formatter: logging formatter - it can be: :default or :json - Defaults to :default

Versioning

Hanami::Model uses Semantic Versioning 2.0.0

Contributing

  1. Fork it ( https://github.com/hanami/model/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Copyright

Copyright © 2014-2021 Luca Guidi – Released under MIT License

This project was formerly known as Lotus (lotus-model).


Author:  hanami
Source code: https://github.com/hanami/model
License: MIT license

#ruby  #ruby-on-rails

Ruby Persistence Framework with Entities and Repositories
Brook  Hudson

Brook Hudson

1659396000

Humidifier: A Ruby tool for Managing AWS CloudFormation Stacks

Humidifier 

Humidifier is a ruby tool for managing AWS CloudFormation stacks. You can use it to build and manage stacks programmatically or you can use it as a command line tool to manage stacks through configuration files.

Installation

Add this line to your application's Gemfile:

gem 'humidifier'

And then execute:

$ bundle

Or install it yourself as:

$ gem install humidifier

Getting started

Stacks are represented by the Humidifier::Stack class. You can set any of the top-level JSON attributes (such as name and description) through the initializer.

Resources are represented by an exact mapping from AWS resource names to Humidifier resources names (e.g. AWS::EC2::Instance becomes Humidifier::EC2::Instance). Resources have accessors for each JSON attribute. Each attribute can also be set through the initialize, update, and update_attribute methods.

Example usage

The below example will create a stack with two resources, a loader balancer and an auto scaling group. It then deploys the new stack and pauses execution until the stack is finished being created.

stack = Humidifier::Stack.new(name: 'Example-Stack')

stack.add(
  'LoaderBalancer',
  Humidifier::ElasticLoadBalancing::LoadBalancer.new(
    scheme: 'internal',
    listeners: [
      {
        load_balancer_port: 80,
        protocol: 'http',
        instance_port: 80,
        instance_protocol: 'http'
      }
    ]
  )
)

stack.add(
  'AutoScalingGroup',
  Humidifier::AutoScaling::AutoScalingGroup.new(
    min_size: '1',
    max_size: '20',
    availability_zones: ['us-east-1a'],
    load_balancer_names: [Humidifier.ref('LoadBalancer')]
  )
)

stack.deploy_and_wait

Interfacing with AWS

Once stacks have the appropriate resources, you can query AWS to handle all stack CRUD operations. The operations themselves are intuitively named (i.e. #create, #update, #delete). There are also convenience methods for validating a stack body (#valid?), checking the existence of a stack (#exists?), and creating or updating based on existence (#deploy).

There are additionally four functions on Humidifier::Stack that support waiting for execution in AWS to finish. They all have non-blocking corollaries, and are named after them. They are: #create_and_wait, #update_and_wait, #delete_and_wait, and #deploy_and_wait.

CloudFormation functions

You can use CFN intrinsic functions and references using Humidifier.fn.[name] and Humidifier.ref. They will build appropriate structures that know how to be dumped to CFN syntax.

Change Sets

Instead of immediately pushing your changes to CloudFormation, Humidifier also supports change sets. Change sets are a powerful feature that allow you to see the changes that will be made before you make them. To read more about change sets see the announcement article. To use them in Humidifier, Humidifier::Stack has the #create_change_set and #deploy_change_set methods. The #create_change_set method will create a change set on the stack. The #deploy_change_set method will create a change set if the stack currently exists, and otherwise will create the stack.

Introspection

To see the template body, you can check the #to_cf method on stacks, resources, fns, and refs. All of them will output a hash of what will be uploaded (except the stack, which will output a string representation).

Humidifier itself contains a registry of all possible resources that it supports. You can access it with Humidifier::registry which is a hash of AWS resource name pointing to the class.

Resources have an ::aws_name method to see how AWS references them. They also contain a ::props method that contains a hash of the name that Humidifier uses to reference the prop pointing to the appropriate prop object.

Large templates

When templates are especially large (larger than 51,200 bytes), they cannot be uploaded directly through the AWS SDK. You can configure Humidifier to seamlessly upload the templates to S3 and reference them using an S3 URL instead by:

Humidifier.configure do |config|
  config.s3_bucket = 'my.s3.bucket'
  config.s3_prefix = 'my-prefix/' # optional
end

Forcing uploading

You can force a stack to upload its template to S3 regardless of the size of the template. This is a useful option if you're going to be deploying multiple copies of a template or if you want a backup. You can set this option on a per-stack basis:

stack.deploy(force_upload: true)

or globally, by setting the configuration option:

Humidifier.configure do |config|
  config.force_upload = true
end

CLI

Humidifier can also be used as a CLI for managing resources through configuration files. For a step-by-step guide, read on, but if you'd like to see a working example, check out the example directory.

To get started, build a ruby script (for example humidifier) that executes the Humidifier::CLI class, like so:

#!/usr/bin/env ruby
require 'humidifier'

Humidifier.configure do |config|
  # optional, defaults to the current working directory, so that all of the
  # directories from the location that you run the CLI are assumed to contain
  # resource specifications
  config.stack_path = 'stacks'

  # optional, a default prefix to use before deploying to AWS
  config.stack_prefix = 'humidifier-'

  # specifies that `users.yml` files contain specifications for `AWS::IAM::User`
  # resources
  config.map :users, to: 'IAM::User'
end

Humidifier::CLI.start(ARGV)

Resource files

Inside of the stacks directory configured above, create a subdirectory for each CloudFormation stack that you want to deploy. With the above configuration, we can create YAML files in the form of users.yml for each stack, which will specify IAM users to create. The file format looks like the below:

EngUser:
  path: /humidifier/
  user_name: EngUser
  groups:
  - Engineering
  - Testing
  - Deployment

AdminUser:
  path: /humidifier/
  user_name: AdminUser
  groups:
  - Management
  - Administration

The top-level keys are the logical resource names that will be displayed in the CloudFormation screen. They point to a map of key/value pairs that will be passed on to humidifier. Any humidifier (and therefore any CloudFormation) attribute may be specified. For more information on CloudFormation templates and which attributes may be specified, see both the humidifier docs and the CloudFormation docs.

Mappers

Oftentimes, specifying these attributes can become repetitive, e.g., each user should automatically receive the same "path" attribute. Other times, you may want custom logic to execute depending on which AWS environment you're running in. Finally, you may want to reference resources in the same or other stacks.

Humidifier's solution for this is to allow customized "mapper" classes to take the user-provided attributes and transform them into the attributes that CloudFormation expects. Consider the following example for mapping a user:

class UserMapper < Humidifier::Config::Mapper
  GROUPS = {
    'eng' => %w[Engineering Testing Deployment],
    'admin' => %w[Management Administration]
  }

  defaults do |logical_name|
    { path: '/humidifier/', user_name: logical_name }
  end

  attribute :group do |group|
    groups = GROUPS[group]
    groups.any? ? { groups: GROUPS[group] } : {}
  end
end

Humidifier.configure do |config|
  config.map :users, to: 'IAM::User', using: UserMapper
end

This means that by default, all entries in the users.yml files will get a /humidifier/ path, the user_name attribute will be set based on the logical name that was provided for the resource, and you can additionally specify a group attribute, even though it is not native to CloudFormation. With this group attribute, it will actually map to the groups attribute that CloudFormation expects.

With this new mapper in place, we can simplify our YAML file to:

EngUser:
  group: eng

AdminUser:
  group: admin

Using the CLI

Now that you've configured your CLI, your resources, and your mappers, you can use the CLI to display, validate, and deploy your infrastructure to CloudFormation. Run your script without any arguments to get the help message and explanations for each command.

Each command has an --aws-profile (or -p) option for specifying which profile to authenticate against when querying AWS. You should ensure that this profile has the correct permissions for creating whatever resources are going to part of your stack. You can also rely on the AWS_* environment variables, or the EC2 instance profile if you're deploying from an instance. For more information, see the AWS docs under the "Configuration" section.

Below are the list of commands and some of their options.

change [?stack]

Creates a change set for either the specified stack or all stacks in the repo. The change set represents the changes between what is currently deployed versus the resources represented by the configuration.

deploy [?stack] [*parameters]

Creates or updates (depending on if the stack already exists) one or all stacks in the repo.

The deploy command also allows a --prefix command line argument that will override the default prefix (if one is configured) for the stack that is being deployed. This is especially useful when you're deploying multiple copies of the same stack (for instance, multiple autoscaling groups) that have different purposes or semantically mean newer versions of resources.

display [stack] [?pattern]

Displays the specified stack in JSON format on the command line. If you optionally pass a pattern argument, it will filter the resources down to just ones whose names match the given pattern.

stacks

Displays the names of all of the stacks that humidifier is managing.

upgrade

Downloads the latest CloudFormation resource specification. Periodically AWS will update the file that humidifier is based on, in which case the attributes of the resources that were changed could change. This gem usually stays relatively in sync, but if you need to use the latest specs and this gem has not yet released a new version containing them, then you can run this command to download the latest specs onto your system.

upload [?stack]

Upload one or all stacks in the repo to S3 for reference later. Note that this must be combined with the humidifier s3_bucket configuration option.

validate [?stack]

Validate that one or all stacks in the repo are properly configured and using values that CloudFormation understands.

version

Output the version of Humidifier as well as the version of the CloudFormation resource specification that you are using.

Parameters

CloudFormation template parameters can be specified by having a special parameters.yml file in your stack directory. This file should contain a YAML-encoded object whose keys are the names of the parameters and whose values are the parameter configuration (using the same underscore paradigm as humidifier resources for specifying configuration).

You can pass values to the CLI deploy command after the stack name on the command line as in:

humidifier deploy foobar Param1=Foo Param2=Bar

Those parameters will get passed in as values when the stack is deployed.

Shortcuts

A couple of convenient shortcuts are built into humidifier so that writing templates and mappers both can be more concise.

Automatic id properties

There are a lot of properties in the AWS CloudFormation resource specification that are simply pointers to other entities within the AWS ecosystem. For example, an AWS::EC2::VPCGatewayAttachment entity has a VpcId property that represents the ID of the associated AWS::EC2::VPC.

Because this pattern is so common, humidifier detects all properties ending in Id and allows you to specify them without the suffix. If you choose to use this format, humidifier will automatically turn that value into a CloudFormation resource reference.

Anonymous mappers

A lot of the time, mappers that you create will not be overly complicated, especially if you're using automatic id properties. So, the config.map method optionally takes a block, and allows you to specify the mapper inline. This is recommended for mappers that aren't too complicated as to warrant their own class (for instance, for testing purposes). An example of this using the UserMapper from above is below:

Humidifier.configure do |config|
  config.map :users, to: 'IAM::User' do
    GROUPS = {
      'eng' => %w[Engineering Testing Deployment],
      'admin' => %w[Management Administration]
    }

    defaults do |logical_name|
      { path: '/humidifier/', user_name: logical_name }
    end

    attribute :group do |group|
      groups = GROUPS[group]
      groups.any? ? { groups: GROUPS[group] } : {}
    end
  end
end

Cross-stack references

AWS allows cross-stack references through the intrinsic Fn::ImportValue function. You can take advantage of this with humidifier by using the export: true option on resources in your stacks. For instance, if in one stack you have a subnet that you need to reference in another, you could (stacks/vpc/subnets.yml):

ProductionPrivateSubnet2a:
  vpc: ProductionVPC
  cidr_block: 10.0.0.0/19
  availability_zone: us-west-2a
  export: true

ProductionPrivateSubnet2b:
  vpc: ProductionVPC
  cidr_block: 10.0.64.0/19
  availability_zone: us-west-2b
  export: true

ProductionPrivateSubnet2c:
  vpc: ProductionVPC
  cidr_block: 10.0.128.0/19
  availability_zone: us-west-2c
  export: true

And then in another stack, you could reference those values (stacks/rds/db_subnets_groups.yml):

ProductionDBSubnetGroup:
  db_subnet_group_description: Production DB private subnet group
  subnets:
  - ProductionPrivateSubnet2a
  - ProductionPrivateSubnet2b
  - ProductionPrivateSubnet2c

Within the configuration, you would specify to use the Fn::ImportValue function like so:

Humidifier.configure do |config|
  config.stack_path = 'stacks'

  config.map :subnets, to: 'EC2::Subnet'

  config.map :db_subnet_groups, to: 'RDS::DBSubnetGroup' do
    attribute :subnets do |subnet_names|
      subnet_ids =
        subnet_names.map do |subnet_name|
          Humidifier.fn.import_value(subnet_name)
        end

      { subnet_ids: subnet_ids }
    end
  end
end

If you specify export: true it will by default export a reference to the resource listed in the stack. You can also choose to export a different attribute by specifying the attribute as the value to export. For example, if we were creating instance profiles and wanted to export the Arn so that it could be referenced by an instance later, we could:

APIRoleInstanceProfile:
  depends_on: APIRole
  roles:
  - APIRole
  export: Arn

Development

To get started, ensure you have ruby installed, version 2.4 or later. From there, install the bundler gem: gem install bundler and then bundle install in the root of the repository.

Testing

The default rake task runs the tests. Styling is governed by rubocop. The docs are generated with yard. To run all three of these, run:

$ bundle exec rake
$ bundle exec rubocop
$ bundle exec rake yard

Specs

The specs pulled from the CFN docs is saved to CloudFormationResourceSpecification.json. You can update it by running bundle exec rake specs. This script will pull down the latest resource specification to be used with Humidifier.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/kddnewton/humidifier.

License

The gem is available as open source under the terms of the MIT License.


Author: kddnewton
Source code: https://github.com/kddnewton/humidifier
License: MIT license

#ruby  #ruby-on-rails 

Humidifier: A Ruby tool for Managing AWS CloudFormation Stacks
Brook  Legros

Brook Legros

1659192480

Hashie: A Collection Of Classes and Mixins That Make Ruby Hashes

Hashie

 Hashie is a growing collection of tools that extend Hashes and make them more useful.

Installation

Hashie is available as a RubyGem:

$ gem install hashie

Stable Release

You're reading the documentation for the next release of Hashie, which should be 5.0.1. The current stable release is 5.0.0.

Hash Extensions

The library is broken up into a number of atomically includable Hash extension modules as described below. This provides maximum flexibility for users to mix and match functionality while maintaining feature parity with earlier versions of Hashie.

Any of the extensions listed below can be mixed into a class by include-ing Hashie::Extensions::ExtensionName.

Logging

Hashie has a built-in logger that you can override. By default, it logs to STDOUT but can be replaced by any Logger class. The logger is accessible on the Hashie module, as shown below:

# Set the logger to the Rails logger
Hashie.logger = Rails.logger

Coercion

Coercions allow you to set up "coercion rules" based either on the key or the value type to massage data as it's being inserted into the Hash. Key coercions might be used, for example, in lightweight data modeling applications such as an API client:

class Tweet < Hash
  include Hashie::Extensions::Coercion
  include Hashie::Extensions::MergeInitializer
  coerce_key :user, User
end

user_hash = { name: "Bob" }
Tweet.new(user: user_hash)
# => automatically calls User.coerce(user_hash) or
#    User.new(user_hash) if that isn't present.

Value coercions, on the other hand, will coerce values based on the type of the value being inserted. This is useful if you are trying to build a Hash-like class that is self-propagating.

class SpecialHash < Hash
  include Hashie::Extensions::Coercion
  coerce_value Hash, SpecialHash

  def initialize(hash = {})
    super
    hash.each_pair do |k,v|
      self[k] = v
    end
  end
end

Coercing Collections

class Tweet < Hash
  include Hashie::Extensions::Coercion
  coerce_key :mentions, Array[User]
  coerce_key :friends, Set[User]
end

user_hash = { name: "Bob" }
mentions_hash= [user_hash, user_hash]
friends_hash = [user_hash]
tweet = Tweet.new(mentions: mentions_hash, friends: friends_hash)
# => automatically calls User.coerce(user_hash) or
#    User.new(user_hash) if that isn't present on each element of the array

tweet.mentions.map(&:class) # => [User, User]
tweet.friends.class # => Set

Coercing Hashes

class Relation
  def initialize(string)
    @relation = string
  end
end

class Tweet < Hash
  include Hashie::Extensions::Coercion
  coerce_key :relations, Hash[User => Relation]
end

user_hash = { name: "Bob" }
relations_hash= { user_hash => "father", user_hash => "friend" }
tweet = Tweet.new(relations: relations_hash)
tweet.relations.map { |k,v| [k.class, v.class] } # => [[User, Relation], [User, Relation]]
tweet.relations.class # => Hash

# => automatically calls User.coerce(user_hash) on each key
#    and Relation.new on each value since Relation doesn't define the `coerce` class method

Coercing Core Types

Hashie handles coercion to the following by using standard conversion methods:

typemethod
Integer#to_i
Float#to_f
Complex#to_c
Rational#to_r
String#to_s
Symbol#to_sym

Note: The standard Ruby conversion methods are less strict than you may assume. For example, :foo.to_i raises an error but "foo".to_i returns 0.

You can also use coerce from the following supertypes with coerce_value:

  • Integer
  • Numeric

Hashie does not have built-in support for coercing boolean values, since Ruby does not have a built-in boolean type or standard method for coercing to a boolean. You can coerce to booleans using a custom proc.

Coercion Proc

You can use a custom coercion proc on either #coerce_key or #coerce_value. This is useful for coercing to booleans or other simple types without creating a new class and coerce method. For example:

class Tweet < Hash
  include Hashie::Extensions::Coercion
  coerce_key :retweeted, ->(v) do
    case v
    when String
      !!(v =~ /\A(true|t|yes|y|1)\z/i)
    when Numeric
      !v.to_i.zero?
    else
      v == true
    end
  end
end

A note on circular coercion

Since coerce_key is a class-level method, you cannot have circular coercion without the use of a proc. For example:

class CategoryHash < Hash
  include Hashie::Extensions::Coercion
  include Hashie::Extensions::MergeInitializer

  coerce_key :products, Array[ProductHash]
end

class ProductHash < Hash
  include Hashie::Extensions::Coercion
  include Hashie::Extensions::MergeInitializer

  coerce_key :categories, Array[CategoriesHash]
end

This will fail with a NameError for CategoryHash::ProductHash because ProductHash is not defined at the point that coerce_key is happening for CategoryHash.

To work around this, you can use a coercion proc. For example, you could do:

class CategoryHash < Hash
  # ...
  coerce_key :products, ->(value) do
    return value.map { |v| ProductHash.new(v) } if value.respond_to?(:map)

    ProductHash.new(value)
  end
end

KeyConversion

The KeyConversion extension gives you the convenience methods of symbolize_keys and stringify_keys along with their bang counterparts. You can also include just stringify or just symbolize with Hashie::Extensions::StringifyKeys or Hashie::Extensions::SymbolizeKeys.

Hashie also has a utility method for converting keys on a Hash without a mixin:

Hashie.symbolize_keys! hash # => Symbolizes all string keys of hash.
Hashie.symbolize_keys hash # => Returns a copy of hash with string keys symbolized.
Hashie.stringify_keys! hash # => Stringifies keys of hash.
Hashie.stringify_keys hash # => Returns a copy of hash with keys stringified.

MergeInitializer

The MergeInitializer extension simply makes it possible to initialize a Hash subclass with another Hash, giving you a quick short-hand.

MethodAccess

The MethodAccess extension allows you to quickly build method-based reading, writing, and querying into your Hash descendant. It can also be included as individual modules, i.e. Hashie::Extensions::MethodReader, Hashie::Extensions::MethodWriter and Hashie::Extensions::MethodQuery.

class MyHash < Hash
  include Hashie::Extensions::MethodAccess
end

h = MyHash.new
h.abc = 'def'
h.abc  # => 'def'
h.abc? # => true

MethodAccessWithOverride

The MethodAccessWithOverride extension is like the MethodAccess extension, except that it allows you to override Hash methods. It aliases any overridden method with two leading underscores. To include only this overriding functionality, you can include the single module Hashie::Extensions::MethodOverridingWriter.

class MyHash < Hash
  include Hashie::Extensions::MethodAccess
end

class MyOverridingHash < Hash
  include Hashie::Extensions::MethodAccessWithOverride
end

non_overriding = MyHash.new
non_overriding.zip = 'a-dee-doo-dah'
non_overriding.zip #=> [[['zip', 'a-dee-doo-dah']]]

overriding = MyOverridingHash.new
overriding.zip = 'a-dee-doo-dah'
overriding.zip   #=> 'a-dee-doo-dah'
overriding.__zip #=> [[['zip', 'a-dee-doo-dah']]]

MethodOverridingInitializer

The MethodOverridingInitializer extension will override hash methods if you pass in a normal hash to the constructor. It aliases any overridden method with two leading underscores. To include only this initializing functionality, you can include the single module Hashie::Extensions::MethodOverridingInitializer.

class MyHash < Hash
end

class MyOverridingHash < Hash
  include Hashie::Extensions::MethodOverridingInitializer
end

non_overriding = MyHash.new(zip: 'a-dee-doo-dah')
non_overriding.zip #=> []

overriding = MyOverridingHash.new(zip: 'a-dee-doo-dah')
overriding.zip   #=> 'a-dee-doo-dah'
overriding.__zip #=> [[['zip', 'a-dee-doo-dah']]]

IndifferentAccess

This extension can be mixed in to your Hash subclass to allow you to use Strings or Symbols interchangeably as keys; similar to the params hash in Rails.

In addition, IndifferentAccess will also inject itself into sub-hashes so they behave the same.

class MyHash < Hash
  include Hashie::Extensions::MergeInitializer
  include Hashie::Extensions::IndifferentAccess
end

myhash = MyHash.new(:cat => 'meow', 'dog' => 'woof')
myhash['cat'] # => "meow"
myhash[:cat]  # => "meow"
myhash[:dog]  # => "woof"
myhash['dog'] # => "woof"

# Auto-Injecting into sub-hashes.
myhash['fishes'] = {}
myhash['fishes'].class # => Hash
myhash['fishes'][:food] = 'flakes'
myhash['fishes']['food'] # => "flakes"

To get back a normal, not-indifferent Hash, you can use #to_hash on the indifferent hash. It exports the keys as strings, not symbols:

myhash = MyHash.new
myhash["foo"] = "bar"
myhash[:foo]  #=> "bar"

normal_hash = myhash.to_hash
myhash["foo"]  #=> "bar"
myhash[:foo]  #=> nil

IgnoreUndeclared

This extension can be mixed in to silently ignore undeclared properties on initialization instead of raising an error. This is useful when using a Trash to capture a subset of a larger hash.

class Person < Trash
  include Hashie::Extensions::IgnoreUndeclared
  property :first_name
  property :last_name
end

user_data = {
  first_name: 'Freddy',
  last_name: 'Nostrils',
  email: 'freddy@example.com'
}

p = Person.new(user_data) # 'email' is silently ignored

p.first_name # => 'Freddy'
p.last_name  # => 'Nostrils'
p.email      # => NoMethodError

DeepMerge

This extension allows you to easily include a recursive merging system into any Hash descendant:

class MyHash < Hash
  include Hashie::Extensions::DeepMerge
end

h1 = MyHash[{ x: { y: [4,5,6] }, z: [7,8,9] }]
h2 = MyHash[{ x: { y: [7,8,9] }, z: "xyz" }]

h1.deep_merge(h2) # => { x: { y: [7, 8, 9] }, z: "xyz" }
h2.deep_merge(h1) # => { x: { y: [4, 5, 6] }, z: [7, 8, 9] }

Like with Hash#merge in the standard library, a block can be provided to merge values:

class MyHash < Hash
  include Hashie::Extensions::DeepMerge
end

h1 = MyHash[{ a: 100, b: 200, c: { c1: 100 } }]
h2 = MyHash[{ b: 250, c: { c1: 200 } }]

h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
# => { a: 100, b: 450, c: { c1: 300 } }

DeepFetch

This extension can be mixed in to provide for safe and concise retrieval of deeply nested hash values. In the event that the requested key does not exist a block can be provided and its value will be returned.

Though this is a hash extension, it conveniently allows for arrays to be present in the nested structure. This feature makes the extension particularly useful for working with JSON API responses.

user = {
  name: { first: 'Bob', last: 'Boberts' },
  groups: [
    { name: 'Rubyists' },
    { name: 'Open source enthusiasts' }
  ]
}

user.extend Hashie::Extensions::DeepFetch

user.deep_fetch :name, :first # => 'Bob'
user.deep_fetch :name, :middle # => 'KeyError: Could not fetch middle'

# using a default block
user.deep_fetch(:name, :middle) { |key| 'default' }  # =>  'default'

# a nested array
user.deep_fetch :groups, 1, :name # => 'Open source enthusiasts'

DeepFind

This extension can be mixed in to provide for concise searching for keys within a deeply nested hash.

It can also search through any Enumerable contained within the hash for objects with the specified key.

Note: The searches are depth-first, so it is not guaranteed that a shallowly nested value will be found before a deeply nested value.

user = {
  name: { first: 'Bob', last: 'Boberts' },
  groups: [
    { name: 'Rubyists' },
    { name: 'Open source enthusiasts' }
  ]
}

user.extend Hashie::Extensions::DeepFind

user.deep_find(:name)   #=> { first: 'Bob', last: 'Boberts' }
user.deep_detect(:name) #=> { first: 'Bob', last: 'Boberts' }

user.deep_find_all(:name) #=> [{ first: 'Bob', last: 'Boberts' }, 'Rubyists', 'Open source enthusiasts']
user.deep_select(:name)   #=> [{ first: 'Bob', last: 'Boberts' }, 'Rubyists', 'Open source enthusiasts']

DeepLocate

This extension can be mixed in to provide a depth first search based search for enumerables matching a given comparator callable.

It returns all enumerables which contain at least one element, for which the given comparator returns true.

Because the container objects are returned, the result elements can be modified in place. This way, one can perform modifications on deeply nested hashes without the need to know the exact paths.


books = [
  {
    title: "Ruby for beginners",
    pages: 120
  },
  {
    title: "CSS for intermediates",
    pages: 80
  },
  {
    title: "Collection of ruby books",
    books: [
      {
        title: "Ruby for the rest of us",
        pages: 576
      }
    ]
  }
]

books.extend(Hashie::Extensions::DeepLocate)

# for ruby 1.9 leave *no* space between the lambda rocket and the braces
# http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/

books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") }
# => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"Ruby for the rest of us", :pages=>576}]

books.deep_locate -> (key, value, object) { key == :pages && value <= 120 }
# => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"CSS for intermediates", :pages=>80}]

StrictKeyAccess

This extension can be mixed in to allow a Hash to raise an error when attempting to extract a value using a non-existent key.

class StrictKeyAccessHash < Hash
  include Hashie::Extensions::StrictKeyAccess
end

>> hash = StrictKeyAccessHash[foo: "bar"]
=> {:foo=>"bar"}
>> hash[:foo]
=> "bar"
>> hash[:cow]
  KeyError: key not found: :cow

Mash

Mash is an extended Hash that gives simple pseudo-object functionality that can be built from hashes and easily extended. It is intended to give the user easier access to the objects within the Mash through a property-like syntax, while still retaining all Hash functionality.

mash = Hashie::Mash.new
mash.name? # => false
mash.name # => nil
mash.name = "My Mash"
mash.name # => "My Mash"
mash.name? # => true
mash.inspect # => <Hashie::Mash name="My Mash">

mash = Hashie::Mash.new
# use bang methods for multi-level assignment
mash.author!.name = "Michael Bleigh"
mash.author # => <Hashie::Mash name="Michael Bleigh">

mash = Hashie::Mash.new
# use under-bang methods for multi-level testing
mash.author_.name? # => false
mash.inspect # => <Hashie::Mash>

Note: The ? method will return false if a key has been set to false or nil. In order to check if a key has been set at all, use the mash.key?('some_key') method instead.

How does Mash handle conflicts with pre-existing methods?

Please note that a Mash will not override methods through the use of the property-like syntax. This can lead to confusion if you expect to be able to access a Mash value through the property-like syntax for a key that conflicts with a method name. However, it protects users of your library from the unexpected behavior of those methods being overridden behind the scenes.

mash = Hashie::Mash.new
mash.name = "My Mash"
mash.zip = "Method Override?"
mash.zip # => [[["name", "My Mash"]], [["zip", "Method Override?"]]]

Since Mash gives you the ability to set arbitrary keys that then act as methods, Hashie logs when there is a conflict between a key and a pre-existing method. You can set the logger that this logs message to via the global Hashie logger:

Hashie.logger = Rails.logger

You can also disable the logging in subclasses of Mash:

class Response < Hashie::Mash
  disable_warnings
end

The default is to disable logging for all methods that conflict. If you would like to only disable the logging for specific methods, you can include an array of method keys:

class Response < Hashie::Mash
  disable_warnings :zip, :zap
end

This behavior is cumulative. The examples above and below behave identically.

class Response < Hashie::Mash
  disable_warnings :zip
  disable_warnings :zap
end

Disable warnings will honor the last disable_warnings call. Calling without parameters will override the ignored methods list, and calling with parameters will create a new ignored methods list. This includes child classes that inherit from a class that disables warnings.

class Message < Hashie::Mash
  disable_warnings :zip, :zap
  disable_warnings
end

# No errors will be logged
Message.new(merge: 'true', compact: true)
class Message < Hashie::Mash
  disable_warnings
end

class Response < Message
  disable_warnings :zip, :zap
end

# 2 errors will be logged
Response.new(merge: 'true', compact: true, zip: '90210', zap: 'electric')

If you would like to create an anonymous subclass of a Hashie::Mash with key conflict warnings disabled:

Hashie::Mash.quiet.new(zip: '90210', compact: true) # no errors logged
Hashie::Mash.quiet(:zip).new(zip: '90210', compact: true) # error logged for compact

How does the wrapping of Mash sub-Hashes work?

Mash duplicates any sub-Hashes that you add to it and wraps them in a Mash. This allows for infinite chaining of nested Hashes within a Mash without modifying the object(s) that are passed into the Mash. When you subclass Mash, the subclass wraps any sub-Hashes in its own class. This preserves any extensions that you mixed into the Mash subclass and allows them to work within the sub-Hashes, in addition to the main containing Mash.

mash = Hashie::Mash.new(name: "Hashie", dependencies: { rake: "< 11", rspec: "~> 3.0" })
mash.dependencies.class #=> Hashie::Mash

class MyGem < Hashie::Mash; end
my_gem = MyGem.new(name: "Hashie", dependencies: { rake: "< 11", rspec: "~> 3.0" })
my_gem.dependencies.class #=> MyGem

How does Mash handle key types which cannot be symbolized?

Mash preserves keys which cannot be converted directly to both a string and a symbol, such as numeric keys. Since Mash is conceived to provide psuedo-object functionality, handling keys which cannot represent a method call falls outside its scope of value.

Hashie::Mash.new('1' => 'one string', :'1' => 'one sym', 1 => 'one num')
# => {"1"=>"one sym", 1=>"one num"}

The symbol key :'1' is converted the string '1' to support indifferent access and consequently its value 'one sym' will override the previously set 'one string'. However, the subsequent key of 1 cannot directly convert to a symbol and therefore not converted to the string '1' that would otherwise override the previously set value of 'one sym'.

What else can Mash do?

Mash allows you also to transform any files into a Mash objects.

#/etc/config/settings/twitter.yml
development:
  api_key: 'api_key'
production:
  api_key: <%= ENV['API_KEY'] %> #let's say that ENV['API_KEY'] is set to 'abcd'
mash = Mash.load('settings/twitter.yml')
mash.development.api_key # => 'localhost'
mash.development.api_key = "foo" # => <# RuntimeError can't modify frozen ...>
mash.development.api_key? # => true

You can also load with a Pathname object:

mash = Mash.load(Pathname 'settings/twitter.yml')
mash.development.api_key # => 'localhost'

You can access a Mash from another class:

mash = Mash.load('settings/twitter.yml')[ENV['RACK_ENV']]
Twitter.extend mash.to_module # NOTE: if you want another name than settings, call: to_module('my_settings')
Twitter.settings.api_key # => 'abcd'

You can use another parser (by default: YamlErbParser):

#/etc/data/user.csv
id | name          | lastname
---|------------- | -------------
1  |John          | Doe
2  |Laurent       | Garnier
mash = Mash.load('data/user.csv', parser: MyCustomCsvParser)
# => { 1 => { name: 'John', lastname: 'Doe'}, 2 => { name: 'Laurent', lastname: 'Garnier' } }
mash[1] #=> { name: 'John', lastname: 'Doe' }

The Mash#load method calls YAML.safe_load(path, [], [], true).

Specify permitted_symbols, permitted_classes and aliases options as needed.

Mash.load('data/user.csv', permitted_classes: [Symbol], permitted_symbols: [], aliases: false)

KeepOriginalKeys

This extension can be mixed into a Mash to keep the form of any keys passed directly into the Mash. By default, Mash converts symbol keys to strings to give indifferent access. This extension still allows indifferent access, but keeps the form of the keys to eliminate confusion when you're not expecting the keys to change.

class KeepingMash < ::Hashie::Mash
  include Hashie::Extensions::Mash::KeepOriginalKeys
end

mash = KeepingMash.new(:symbol_key => :symbol, 'string_key' => 'string')
mash.to_hash == { :symbol_key => :symbol, 'string_key' => 'string' }  #=> true
mash.symbol_key  #=> :symbol
mash[:symbol_key]  #=> :symbol
mash['symbol_key']  #=> :symbol
mash.string_key  #=> 'string'
mash['string_key']  #=> 'string'
mash[:string_key]  #=> 'string'

PermissiveRespondTo

By default, Mash only states that it responds to built-in methods, affixed methods (e.g. setters, underbangs, etc.), and keys that it currently contains. That means it won't state that it responds to a getter for an unset key, as in the following example:

mash = Hashie::Mash.new(a: 1)
mash.respond_to? :b  #=> false

This means that by default Mash is not a perfect match for use with a SimpleDelegator since the delegator will not forward messages for unset keys to the Mash even though it can handle them.

In order to have a SimpleDelegator-compatible Mash, you can use the PermissiveRespondTo extension to make Mash respond to anything.

class PermissiveMash < Hashie::Mash
  include Hashie::Extensions::Mash::PermissiveRespondTo
end

mash = PermissiveMash.new(a: 1)
mash.respond_to? :b  #=> true

This comes at the cost of approximately 20% performance for initialization and setters and 19KB of permanent memory growth for each such class that you create.

SafeAssignment

This extension can be mixed into a Mash to guard the attempted overwriting of methods by property setters. When mixed in, the Mash will raise an ArgumentError if you attempt to write a property with the same name as an existing method.

class SafeMash < ::Hashie::Mash
  include Hashie::Extensions::Mash::SafeAssignment
end

safe_mash = SafeMash.new
safe_mash.zip   = 'Test' # => ArgumentError
safe_mash[:zip] = 'test' # => still ArgumentError

SymbolizeKeys

This extension can be mixed into a Mash to change the default behavior of converting keys to strings. After mixing this extension into a Mash, the Mash will convert all string keys to symbols. It can be useful to use with keywords argument, which required symbol keys.

class SymbolizedMash < ::Hashie::Mash
  include Hashie::Extensions::Mash::SymbolizeKeys
end

symbol_mash = SymbolizedMash.new
symbol_mash['test'] = 'value'
symbol_mash.test  #=> 'value'
symbol_mash.to_h  #=> {test: 'value'}

def example(test:)
  puts test
end

example(symbol_mash) #=> value

There is a major benefit and coupled with a major trade-off to this decision (at least on older Rubies). As a benefit, by using symbols as keys, you will be able to use the implicit conversion of a Mash via the #to_hash method to destructure (or splat) the contents of a Mash out to a block. This can be handy for doing iterations through the Mash's keys and values, as follows:

symbol_mash = SymbolizedMash.new(id: 123, name: 'Rey')
symbol_mash.each do |key, value|
  # key is :id, then :name
  # value is 123, then 'Rey'
end

However, on Rubies less than 2.0, this means that every key you send to the Mash will generate a symbol. Since symbols are not garbage-collected on older versions of Ruby, this can cause a slow memory leak when using a symbolized Mash with data generated from user input.

DefineAccessors

This extension can be mixed into a Mash so it makes it behave like OpenStruct. It reduces the overhead of method_missing? magic by lazily defining field accessors when they're requested.

class MyHash < ::Hashie::Mash
  include Hashie::Extensions::Mash::DefineAccessors
end

mash = MyHash.new
MyHash.method_defined?(:foo=) #=> false
mash.foo = 123
MyHash.method_defined?(:foo=) #=> true

MyHash.method_defined?(:foo) #=> false
mash.foo #=> 123
MyHash.method_defined?(:foo) #=> true

You can also extend the existing mash without defining a class:

mash = ::Hashie::Mash.new.with_accessors!

Dash

Dash is an extended Hash that has a discrete set of defined properties and only those properties may be set on the hash. Additionally, you can set defaults for each property. You can also flag a property as required. Required properties will raise an exception if unset. Another option is message for required properties, which allow you to add custom messages for required property. A property with a proc value will be evaluated lazily upon retrieval.

You can also conditionally require certain properties by passing a Proc or Symbol. If a Proc is provided, it will be run in the context of the Dash instance. If a Symbol is provided, the value returned for the property or method of the same name will be evaluated. The property will be required if the result of the conditional is truthy.

class Person < Hashie::Dash
  property :name, required: true
  property :age, required: true, message: 'must be set.'
  property :email
  property :phone, required: -> { email.nil? }, message: 'is required if email is not set.'
  property :pants, required: :weekday?, message: 'are only required on weekdays.'
  property :occupation, default: 'Rubyist'
  property :genome

  def weekday?
    [ Time.now.saturday?, Time.now.sunday? ].none?
  end
end

p = Person.new # => ArgumentError: The property 'name' is required for this Dash.
p = Person.new(name: 'Bob') # => ArgumentError: The property 'age' must be set.

p = Person.new(name: "Bob", age: 18)
p.name         # => 'Bob'
p.name = nil   # => ArgumentError: The property 'name' is required for this Dash.
p.age          # => 18
p.age = nil    # => ArgumentError: The property 'age' must be set.
p.email = 'abc@def.com'
p.occupation   # => 'Rubyist'
p.email        # => 'abc@def.com'
p[:awesome]    # => NoMethodError
p[:occupation] # => 'Rubyist'
p.update_attributes!(name: 'Trudy', occupation: 'Evil')
p.occupation   # => 'Evil'
p.name         # => 'Trudy'
p.update_attributes!(occupation: nil)
p.occupation   # => 'Rubyist'
p.genome = -> { Genome.sequence } # Some expensive operation
p.genome       # => 'GATTACA'

Properties defined as symbols are not the same thing as properties defined as strings.

class Tricky < Hashie::Dash
  property :trick
  property 'trick'
end

p = Tricky.new(trick: 'one', 'trick' => 'two')
p.trick # => 'one', always symbol version
p[:trick] # => 'one'
p['trick'] # => 'two'

Note that accessing a property as a method always uses the symbol version.

class Tricky < Hashie::Dash
  property 'trick'
end

p = Tricky.new('trick' => 'two')
p.trick # => NoMethodError

If you would like to update a Dash and use any default values set in the case of a nil value, use #update_attributes!.

class WithDefaults < Hashie::Dash
  property :description, default: 'none'
end

dash = WithDefaults.new
dash.description  #=> 'none'

dash.description = 'You committed one of the classic blunders!'
dash.description  #=> 'You committed one of the classic blunders!'

dash.description = nil
dash.description  #=> nil

dash.description = 'Only slightly less known is ...'
dash.update_attributes!(description: nil)
dash.description  #=> 'none'

Potential Gotchas

Because Dashes are subclasses of the built-in Ruby Hash class, the double-splat operator takes the Dash as-is without any conversion. This can lead to strange behavior when you use the double-splat operator on a Dash as the first part of a keyword list or built Hash. For example:

class Foo < Hashie::Dash
  property :bar
end

foo = Foo.new(bar: 'baz')      #=> {:bar=>"baz"}
qux = { **foo, quux: 'corge' } #=> {:bar=> "baz", :quux=>"corge"}
qux.is_a?(Foo)                 #=> true
qux[:quux]
#=> raise NoMethodError, "The property 'quux' is not defined for Foo."
qux.key?(:quux) #=> true

You can work around this problem in two ways:

  1. Call #to_h on the resulting object to convert it into a Hash.
  2. Use the double-splat operator on the Dash as the last argument in the Hash literal. This will cause the resulting object to be a Hash instead of a Dash, thereby circumventing the problem.
qux = { **foo, quux: 'corge' }.to_h #=> {:bar=> "baz", :quux=>"corge"}
qux.is_a?(Hash)                     #=> true
qux[:quux]                          #=> "corge"

qux = { quux: 'corge', **foo } #=> {:quux=>"corge", :bar=> "baz"}
qux.is_a?(Hash)                #=> true
qux[:quux]                     #=> "corge"

PropertyTranslation

The Hashie::Extensions::Dash::PropertyTranslation mixin extends a Dash with the ability to remap keys from a source hash.

Property translation is useful when you need to read data from another application -- such as a Java API -- where the keys are named differently from Ruby conventions.

class PersonHash < Hashie::Dash
  include Hashie::Extensions::Dash::PropertyTranslation

  property :first_name, from: :firstName
  property :last_name, from: :lastName
  property :first_name, from: :f_name
  property :last_name, from: :l_name
end

person = PersonHash.new(firstName: 'Michael', l_name: 'Bleigh')
person[:first_name]  #=> 'Michael'
person[:last_name]   #=> 'Bleigh

You can also use a lambda to translate the value. This is particularly useful when you want to ensure the type of data you're wrapping.

class DataModelHash < Hashie::Dash
  include Hashie::Extensions::Dash::PropertyTranslation

  property :id, transform_with: ->(value) { value.to_i }
  property :created_at, from: :created, with: ->(value) { Time.parse(value) }
end

model = DataModelHash.new(id: '123', created: '2014-04-25 22:35:28')
model.id.class          #=> Integer (Fixnum if you are using Ruby 2.3 or lower)
model.created_at.class  #=> Time

Mash and Rails 4 Strong Parameters

To enable compatibility with Rails 4 use the hashie-forbidden_attributes gem.

Coercion

If you want to use Hashie::Extensions::Coercion together with Dash then you may probably want to use Hashie::Extensions::Dash::Coercion instead. This extension automatically includes Hashie::Extensions::Coercion and also adds a convenient :coerce option to property so you can define coercion in one line instead of using property and coerce_key separate:

class UserHash < Hashie::Dash
  include Hashie::Extensions::Coercion

  property :id
  property :posts

  coerce_key :posts, Array[PostHash]
end

This is the same as:

class UserHash < Hashie::Dash
  include Hashie::Extensions::Dash::Coercion

  property :id
  property :posts, coerce: Array[PostHash]
end

PredefinedValues

The Hashie::Extensions::Dash::PredefinedValues mixin extends a Dash with the ability to accept predefined values on a property.

class UserHash < Hashie::Dash
  include Hashie::Extensions::Dash::PredefinedValues

  property :gender, values: %i[male female prefer_not_to_say]
  property :age, values: (0..150)
end

Trash

A Trash is a Dash that allows you to translate keys on initialization. It mixes in the PropertyTranslation mixin by default and is used like so:

class Person < Hashie::Trash
  property :first_name, from: :firstName
end

This will automatically translate the firstName key to first_name when it is initialized using a hash such as through:

Person.new(firstName: 'Bob')

Trash also supports translations using lambda, this could be useful when dealing with external API's. You can use it in this way:

class Result < Hashie::Trash
  property :id, transform_with: lambda { |v| v.to_i }
  property :created_at, from: :creation_date, with: lambda { |v| Time.parse(v) }
end

this will produce the following

result = Result.new(id: '123', creation_date: '2012-03-30 17:23:28')
result.id.class         # => Integer (Fixnum if you are using Ruby 2.3 or lower)
result.created_at.class # => Time

Clash

Clash is a Chainable Lazy Hash that allows you to easily construct complex hashes using method notation chaining. This will allow you to use a more action-oriented approach to building options hashes.

Essentially, a Clash is a generalized way to provide much of the same kind of "chainability" that libraries like Arel or Rails 2.x's named_scopes provide.

c = Hashie::Clash.new
c.where(abc: 'def').order(:created_at)
c # => { where: { abc: 'def' }, order: :created_at }

# You can also use bang notation to chain into sub-hashes,
# jumping back up the chain with _end!
c = Hashie::Clash.new
c.where!.abc('def').ghi(123)._end!.order(:created_at)
c # => { where: { abc: 'def', ghi: 123 }, order: :created_at }

# Multiple hashes are merged automatically
c = Hashie::Clash.new
c.where(abc: 'def').where(hgi: 123)
c # => { where: { abc: 'def', hgi: 123 } }

Rash

Rash is a Hash whose keys can be Regexps or Ranges, which will map many input keys to a value.

A good use case for the Rash is an URL router for a web framework, where URLs need to be mapped to actions; the Rash's keys match URL patterns, while the values call the action which handles the URL.

If the Rash's value is a proc, the proc will be automatically called with the regexp's MatchData (matched groups) as a block argument.


# Mapping names to appropriate greetings
greeting = Hashie::Rash.new( /^Mr./ => "Hello sir!", /^Mrs./ => "Evening, madame." )
greeting["Mr. Steve Austin"] # => "Hello sir!"
greeting["Mrs. Steve Austin"] # => "Evening, madame."

# Mapping statements to saucy retorts
mapper = Hashie::Rash.new(
  /I like (.+)/ => proc { |m| "Who DOESN'T like #{m[1]}?!" },
  /Get off my (.+)!/ => proc { |m| "Forget your #{m[1]}, old man!" }
)
mapper["I like traffic lights"] # => "Who DOESN'T like traffic lights?!"
mapper["Get off my lawn!"]      # => "Forget your lawn, old man!"

Auto-Optimized

Note: The Rash is automatically optimized every 500 accesses (which means that it sorts the list of Regexps, putting the most frequently matched ones at the beginning).

If this value is too low or too high for your needs, you can tune it by setting: rash.optimize_every = n.

Mascot

eierlegende Wollmilchsau Meet Hashie's "offical" mascot, the eierlegende Wollmilchsau!

Contributing

See CONTRIBUTING.md

Copyright

Copyright (c) 2009-2020 Intridea, Inc., and contributors.

MIT License. See LICENSE for details.


Author:  hashie
Source code: https://github.com/hashie/hashie
License: MIT license

#ruby   #ruby-on-rails 

Hashie: A Collection Of Classes and Mixins That Make Ruby Hashes
Nat  Grady

Nat Grady

1658380500

A Fully Featured, Self-hosted Release Server for Electron Apps

Electron Release Server    

A node web server which serves & manages releases of your Electron App, and is fully compatible with Squirrel Auto-updater (which is built into Electron).

Electron Release Server Demo

Note: Despite being advertised as a release server for Electron applications, it would work for any application using Squirrel.

If you host your project on your Github and do not need a UI for your app, then Nuts is probably what you're looking for. Otherwise, you're in the same boat as I was, and you've found the right place!

Advisory Notices

IMPORTANT:

  • The release of Angular 1.6.0 has broken all electron-release-server versions prior to 1.4.2. Please use the instructions under the Maintenance heading below to update your fork! Sorry for the inconvenience.
  • Since release 1.5.0 several models have changed to accommodate new features. Please use the instructions under Migration to update your database! Sorry for the inconvenience.

Features

  • ✨ Docker 🐳 support (thanks to EvgeneOskin)!
  • ✨ Awesome release management interface powered by AngularJS
    • Authenticates with LDAP, easy to modify to another authentication method if needed
  • ✨ Store assets on server disk, or Amazon S3 (with minor modifications)
    • Use pretty much any database for persistence, thanks to Sails & Waterline
  • ✨ Simple but powerful download urls (NOTE: when no assets are uploaded, server returns 404 by default):
    • /download/latest
    • /download/latest/:platform
    • /download/:version
    • /download/:version/:platform
    • /download/:version/:platform/:filename
    • /download/channel/:channel
    • /download/channel/:channel/:platform
    • /download/flavor/:flavor/latest
    • /download/flavor/:flavor/latest/:platform
    • /download/flavor/:flavor/:version
    • /download/flavor/:flavor/:version/:platform
    • /download/flavor/:flavor/:version/:platform/:filename
    • /download/flavor/:flavor/channel/:channel
    • /download/flavor/:flavor/channel/:channel/:platform
  • ✨ Support pre-release channels (beta, alpha, ...)
  • ✨ Support multiple flavors of your app
  • ✨ Auto-updates with Squirrel:
    • Update URLs provided:
      • /update/:platform/:version[/:channel]
      • /update/flavor/:flavor/:platform/:version[/:channel]
    • Mac uses *.dmg and *.zip
    • Windows uses *.exe and *.nupkg
  • ✨ Auto-updates with NSIS differential updates for Windows
  • ✨ Serve the perfect type of assets: .zip for Squirrel.Mac, .nupkg for Squirrel.Windows, .dmg for Mac users, ...
  • ✨ Specify date of availability for releases
  • ✨ Release notes endpoint
    • /notes/:version/:flavor?

NOTE: if you don't provide the appropriate type of file for Squirrel you won't be able to update your app since the update endpoint will not return a JSON. (.zip for Squirrel.Mac, .nupkg for Squirrel.Windows).

Deploy it / Start it

Follow our guide to deploy Electron Release Server.

Auto-updater / Squirrel

This server provides an endpoint for Squirrel auto-updater, it supports both OS X and Windows.

Documentation

Check out the documentation for more details.

Building Releases

I highly recommend using electron-builder for packaging & releasing your applications. Once you have built your app with that, you can upload the artifacts for your users right away!

Maintenance

You should keep your fork up to date with the electron-release-server master.

Doing so is simple, rebase your repo using the commands below.

git remote add upstream https://github.com/ArekSredzki/electron-release-server.git
git fetch upstream
git rebase upstream/master

Credit

This project has been built from the Sails.js up by Arek Sredzki, with inspiration from nuts.

Author: ArekSredzki
Source Code: https://github.com/ArekSredzki/electron-release-server 
License: MIT license

#electron #node #angularjs #update 

A Fully Featured, Self-hosted Release Server for Electron Apps