راهنمای کدنویسی¶
این صفحه راهنمای کدنویسی اودو را معرفی میکند. هدف این راهنما بهبود کیفیت کدهای اپلیکیشنهای اودو است. در واقع، کدنویسی صحیح خوانایی را افزایش میدهد، نگهداری را آسانتر میکند، به رفع اشکال کمک میکند، پیچیدگی را کاهش میدهد و قابلیت اطمینان را ارتقا میدهد. این راهنما باید در هر ماژول جدید و تمام توسعههای جدید اعمال شود.
هشدار
هنگام تغییر فایلهای موجود در نسخه پایدار، سبک اصلی فایل بهطور کامل بر هر دستورالعمل سبک دیگری اولویت دارد. به عبارت دیگر، لطفاً هرگز فایلهای موجود را برای اعمال این دستورالعملها تغییر ندهید. این کار از اختلال در تاریخچه بازبینی خطوط کد جلوگیری میکند. تغییرات باید حداقل ممکن باشد. برای جزئیات بیشتر، به راهنمای درخواست کشش مراجعه کنید.
هشدار
هنگام اصلاح فایلهای موجود در نسخه اصلی (توسعه)، این دستورالعملها را فقط برای کد اصلاحشده یا در صورتی که بیشتر فایل تحت بازبینی باشد اعمال کنید. به عبارت دیگر، ساختار فایلهای موجود را فقط در صورتی تغییر دهید که تحت تغییرات عمده قرار گیرد. در این حالت ابتدا یک تعهد انتقال انجام دهید و سپس تغییرات مربوط به ویژگی را اعمال کنید.
ساختار ماژول¶
هشدار
برای ماژولهایی که توسط جامعه توسعه داده شدهاند، اکیداً توصیه میشود که ماژول خود را با پیشوندی مانند نام شرکت خود نامگذاری کنید.
دایرکتوریها¶
یک ماژول در پوشههای مهم سازماندهی شده است. این پوشهها شامل منطق کسبوکار هستند؛ بررسی آنها باید شما را با هدف ماژول آشنا کند.
داده/ : دمو و دادههای XML
مدلها/ : تعریف مدلها
کنترلکنندهها/ : شامل کنترلکنندهها (مسیرهای HTTP)
نمایشها/ : شامل نمایشها و قالبها است
static/ : شامل داراییهای وب است که به css/, js/, img/, lib/, ... تقسیم شدهاند.
سایر پوشههای اختیاری ماژول را تشکیل میدهند.
ویزارد/ : مدلهای موقت (
models.TransientModel
) و نماهای آنها را گروهبندی میکندگزارش/ : شامل گزارشهای قابل چاپ و مدلهایی بر اساس نماهای SQL است. اشیاء پایتون و نماهای XML در این پوشه قرار دارند.
tests/ : شامل آزمایشهای پایتون است
نامگذاری فایل¶
نامگذاری فایلها برای یافتن سریع اطلاعات در میان تمام افزونههای اودو اهمیت دارد. این بخش توضیح میدهد که چگونه فایلها را در یک ماژول استاندارد اودو نامگذاری کنیم. به عنوان مثال، ما از یک برنامه «نهالستان گیاهان» <https://github.com/tivisse/odoodays-2018/tree/master/plant_nursery> استفاده میکنیم. این برنامه شامل دو مدل اصلی plant.nursery و plant.order است.
در مورد مدلها، منطق کسبوکار را بر اساس مجموعهای از مدلها که به یک مدل اصلی مشترک تعلق دارند، تقسیم کنید. هر مجموعه در یک فایل مشخص قرار میگیرد که نام آن بر اساس مدل اصلی است. اگر فقط یک مدل وجود داشته باشد، نام آن با نام ماژول یکسان است. هر مدل ارثبری شده باید در فایل جداگانهای قرار گیرد تا درک مدلهای تحت تأثیر قرار گرفته را آسانتر کند.
addons/plant_nursery/
|-- models/
| |-- plant_nursery.py (first main model)
| |-- plant_order.py (another main model)
| |-- res_partner.py (inherited Odoo model)
در مورد امنیت، سه فایل اصلی باید استفاده شوند:
اولین مورد تعریف حقوق دسترسی است که در یک فایل
ir.model.access.csv
انجام شده است.گروههای کاربری در
<module>_groups.xml
تعریف شدهاند.قوانین رکورد در
<model>_security.xml
تعریف شدهاند.
addons/plant_nursery/
|-- security/
| |-- ir.model.access.csv
| |-- plant_nursery_groups.xml
| |-- plant_nursery_security.xml
| |-- plant_order_security.xml
در مورد نماها، نماهای بکاند باید مانند مدلها تقسیم شده و با پسوند _views.xml
نامگذاری شوند. نماهای بکاند شامل لیست، فرم، کانبان، فعالیت، نمودار، محوری و ... هستند. برای سهولت تقسیم بر اساس مدل در نماها، منوهای اصلی که به اقدامات خاصی مرتبط نیستند ممکن است به یک فایل اختیاری به نام <module>_menus.xml
استخراج شوند. قالبها (صفحات QWeb که بهویژه برای نمایش در پورتال/وبسایت استفاده میشوند) در فایلهای جداگانهای با نام <model>_templates.xml
قرار داده میشوند.
addons/plant_nursery/
|-- views/
| | -- plant_nursery_menus.xml (optional definition of main menus)
| | -- plant_nursery_views.xml (backend views)
| | -- plant_nursery_templates.xml (portal templates)
| | -- plant_order_views.xml
| | -- plant_order_templates.xml
| | -- res_partner_views.xml
در مورد دادهها، آنها را بر اساس هدف (دمو یا داده) و مدل اصلی تقسیم کنید. نام فایلها باید نام مدل اصلی باشد که با _demo.xml
یا _data.xml
پسوند داشته باشد. به عنوان مثال، برای یک برنامه که دمو و داده برای مدل اصلی خود دارد، همچنین زیرگروهها، فعالیتها و قالبهای ایمیل که همگی به ماژول ایمیل مرتبط هستند:
addons/plant_nursery/
|-- data/
| |-- plant_nursery_data.xml
| |-- plant_nursery_demo.xml
| |-- mail_data.xml
در مورد کنترلرها، به طور کلی همه کنترلرها به یک کنترلر واحد تعلق دارند که در فایلی به نام <module_name>.py
قرار دارد. یک روش قدیمی در Odoo این است که این فایل را main.py
نامگذاری کنند، اما این روش منسوخ شده محسوب میشود. اگر نیاز دارید یک کنترلر موجود را از یک ماژول دیگر به ارث ببرید، این کار را در فایل <inherited_module_name>.py
انجام دهید. به عنوان مثال، افزودن کنترلر پورتال در یک برنامه در فایل portal.py
انجام میشود.
addons/plant_nursery/
|-- controllers/
| |-- plant_nursery.py
| |-- portal.py (inheriting portal/controllers/portal.py)
| |-- main.py (deprecated, replaced by plant_nursery.py)
در مورد فایلهای استاتیک، فایلهای جاوااسکریپت به طور کلی از همان منطق مدلهای پایتون پیروی میکنند. هر مؤلفه باید در فایل جداگانهای با نامی معنادار قرار گیرد. به عنوان مثال، ویجتهای فعالیت در فایل activity.js
از ماژول ایمیل قرار دارند. زیرشاخهها نیز میتوانند برای ساختاردهی 'بسته' ایجاد شوند (برای جزئیات بیشتر به ماژول وب مراجعه کنید). همین منطق باید برای قالبهای ویجتهای JS (فایلهای XML استاتیک) و سبکهای آنها (فایلهای scss) اعمال شود. دادهها (تصاویر، کتابخانهها) را خارج از Odoo لینک نکنید: به جای استفاده از URL برای یک تصویر، آن را در کد پایه کپی کنید.
در مورد ویزاردها، قواعد نامگذاری مشابه مدلهای پایتون است: <transient>.py
و <transient>_views.xml
. هر دو در پوشه ویزارد قرار میگیرند. این نامگذاری از برنامههای قدیمی اودو که از کلیدواژه ویزارد برای مدلهای موقت استفاده میکردند، گرفته شده است.
addons/plant_nursery/
|-- wizard/
| |-- make_plant_order.py
| |-- make_plant_order_views.xml
در مورد گزارشهای آماری که با پایتون / نماهای SQL و نماهای کلاسیک انجام شدهاند، نامگذاری به صورت زیر است:
addons/plant_nursery/
|-- report/
| |-- plant_order_report.py
| |-- plant_order_report_views.xml
در مورد گزارشهای قابل چاپ که عمدتاً شامل آمادهسازی دادهها و نامگذاری قالبهای Qweb هستند، به شرح زیر است:
addons/plant_nursery/
|-- report/
| |-- plant_order_reports.xml (report actions, paperformat, ...)
| |-- plant_order_templates.xml (xml report templates)
بنابراین ساختار کامل درخت ماژول اودو ما به این شکل است
addons/plant_nursery/
|-- __init__.py
|-- __manifest__.py
|-- controllers/
| |-- __init__.py
| |-- plant_nursery.py
| |-- portal.py
|-- data/
| |-- plant_nursery_data.xml
| |-- plant_nursery_demo.xml
| |-- mail_data.xml
|-- models/
| |-- __init__.py
| |-- plant_nursery.py
| |-- plant_order.py
| |-- res_partner.py
|-- report/
| |-- __init__.py
| |-- plant_order_report.py
| |-- plant_order_report_views.xml
| |-- plant_order_reports.xml (report actions, paperformat, ...)
| |-- plant_order_templates.xml (xml report templates)
|-- security/
| |-- ir.model.access.csv
| |-- plant_nursery_groups.xml
| |-- plant_nursery_security.xml
| |-- plant_order_security.xml
|-- static/
| |-- img/
| | |-- my_little_kitten.png
| | |-- troll.jpg
| |-- lib/
| | |-- external_lib/
| |-- src/
| | |-- js/
| | | |-- widget_a.js
| | | |-- widget_b.js
| | |-- scss/
| | | |-- widget_a.scss
| | | |-- widget_b.scss
| | |-- xml/
| | | |-- widget_a.xml
| | | |-- widget_a.xml
|-- views/
| |-- plant_nursery_menus.xml
| |-- plant_nursery_views.xml
| |-- plant_nursery_templates.xml
| |-- plant_order_views.xml
| |-- plant_order_templates.xml
| |-- res_partner_views.xml
|-- wizard/
| |--make_plant_order.py
| |--make_plant_order_views.xml
توجه
نام فایلها باید فقط شامل [a-z0-9_]
(حروف کوچک الفبایی عددی و _
) باشند.
هشدار
از مجوزهای صحیح فایل استفاده کنید: پوشه ۷۵۵ و فایل ۶۴۴.
فایلهای XML¶
فرمت¶
برای اعلام یک رکورد در XML، استفاده از نشانهگذاری record (با استفاده از <record>) توصیه میشود:
قرار دادن ویژگی
id
قبل ازmodel
برای اعلان فیلد، ویژگی
name
در ابتدا قرار میگیرد. سپس مقدار value را یا در برچسبfield
یا در ویژگیeval
قرار دهید، و در نهایت سایر ویژگیها (ویجت، گزینهها و ...) بر اساس اهمیت مرتب میشوند.سعی کنید رکورد را بر اساس مدل گروهبندی کنید. در صورت وجود وابستگی بین اقدام/منو/نماها، این قاعده ممکن است قابل اعمال نباشد.
از نامگذاری تعریفشده در نقطه بعدی استفاده کنید
تگ <data> فقط برای تنظیم دادههای غیرقابل بهروزرسانی با
noupdate=1
استفاده میشود. اگر در فایل فقط دادههای غیرقابل بهروزرسانی وجود داشته باشد، میتوانnoupdate=1
را روی تگ<odoo>
تنظیم کرد و از تنظیم تگ<data>
صرفنظر کرد.
<record id="view_id" model="ir.ui.view">
<field name="name">view.name</field>
<field name="model">object_name</field>
<field name="priority" eval="16"/>
<field name="arch" type="xml">
<list>
<field name="my_field_1"/>
<field name="my_field_2" string="My Label" widget="statusbar" statusbar_visible="draft,sent,progress,done" />
</list>
</field>
</record>
اودو از برچسبهای سفارشی که به عنوان شکر نحوی عمل میکنند، پشتیبانی میکند:
گزینه منو: از آن به عنوان یک میانبر برای تعریف
ir.ui.menu
استفاده کنیدقالب: از آن برای اعلام یک نمای QWeb استفاده کنید که فقط بخش «arch» نمای مورد نظر را نیاز دارد.
این برچسبها نسبت به نماد رکورد ترجیح داده میشوند.
شناسههای XML و نامگذاری¶
امنیت، نما و عملیات¶
از الگوی زیر استفاده کنید:
برای یک منو:
<model_name>_menu
، یا<model_name>_menu_do_stuff
برای زیرمنوها.برای یک نما:
<model_name>_view_<view_type>
، که در آن view_type میتواندkanban
،form
،list
،search
، ... باشد.برای یک اقدام: اقدام اصلی به
<model_name>_action
احترام میگذارد. سایر اقدامات با_<detail>
خاتمه مییابند، که در آن detail یک رشته با حروف کوچک است که به طور خلاصه اقدام را توضیح میدهد. این تنها زمانی استفاده میشود که چندین اقدام برای مدل تعریف شده باشد.برای اقدامات پنجره: نام اقدام را با اطلاعات نمای خاص مانند
<model_name>_action_view_<view_type>
پسوند کنید.برای یک گروه:
<module_name>_group_<group_name>
که در آن group_name نام گروه است، معمولاً 'user'، 'manager' و ...برای یک قانون:
<model_name>_rule_<concerned_group>
که در آن concerned_group نام کوتاه گروه مربوطه است ('user' برای 'model_name_group_user'، 'public' برای کاربر عمومی، 'company' برای قوانین چند شرکتی، ...).
نام باید با شناسه XML یکسان باشد و زیرخطها با نقطه جایگزین شوند. اقدامات باید نامگذاری واقعی داشته باشند زیرا به عنوان نام نمایشی استفاده میشود.
<!-- views -->
<record id="model_name_view_form" model="ir.ui.view">
<field name="name">model.name.view.form</field>
...
</record>
<record id="model_name_view_kanban" model="ir.ui.view">
<field name="name">model.name.view.kanban</field>
...
</record>
<!-- actions -->
<record id="model_name_action" model="ir.act.window">
<field name="name">Model Main Action</field>
...
</record>
<record id="model_name_action_child_list" model="ir.actions.act_window">
<field name="name">Model Access Children</field>
</record>
<!-- menus and sub-menus -->
<menuitem
id="model_name_menu_root"
name="Main Menu"
sequence="5"
/>
<menuitem
id="model_name_menu_action"
name="Sub Menu 1"
parent="module_name.module_name_menu_root"
action="model_name_action"
sequence="10"
/>
<!-- security -->
<record id="module_name_group_user" model="res.groups">
...
</record>
<record id="model_name_rule_public" model="ir.rule">
...
</record>
<record id="model_name_rule_company" model="ir.rule">
...
</record>
ارثبری XML¶
شناسههای XML دیدگاههای ارثبر باید از همان شناسه اصلی رکورد استفاده کنند. این کار به یافتن تمام ارثبریها بهصورت یکجا کمک میکند. از آنجایی که شناسههای نهایی XML با پیشوند ماژولی که آنها را ایجاد میکند مشخص میشوند، هیچ تداخلی وجود ندارد.
نامگذاری باید شامل پسوند .inherit.{details}
باشد تا هدف از بازنویسی هنگام مشاهده نام آن به راحتی قابل درک باشد.
<record id="model_view_form" model="ir.ui.view">
<field name="name">model.view.form.inherit.module2</field>
<field name="inherit_id" ref="module1.model_view_form"/>
...
</record>
نمای اولیه جدید نیازی به پسوند ارثبری ندارند زیرا اینها رکوردهای جدیدی هستند که بر اساس رکورد اول ایجاد شدهاند.
<record id="module2.model_view_form" model="ir.ui.view">
<field name="name">model.view.form.module2</field>
<field name="inherit_id" ref="module1.model_view_form"/>
<field name="mode">primary</field>
...
</record>
پایتون¶
هشدار
فراموش نکنید بخش مشکلات امنیتی را نیز مطالعه کنید تا کدی امن بنویسید.
گزینههای PEP8¶
استفاده از یک لینتر میتواند به نمایش هشدارها یا خطاهای نحوی و معنایی کمک کند. کد منبع اودو تلاش میکند استانداردهای پایتون را رعایت کند، اما برخی از آنها ممکن است نادیده گرفته شوند.
E501: خط بیش از حد طولانی است
E301: انتظار میرفت 1 خط خالی وجود داشته باشد، اما 0 خط یافت شد
E302: انتظار میرفت ۲ خط خالی باشد، ۱ خط یافت شد
واردات¶
واردات به صورت زیر مرتب شدهاند
کتابخانههای خارجی (هر خط یکی، مرتبشده و جداشده در کتابخانه استاندارد پایتون)
واردات
odoo
واردات از ماژولهای Odoo (بهندرت و فقط در صورت لزوم)
در داخل این ۳ گروه، خطوط وارد شده به ترتیب حروف الفبا مرتب شدهاند.
# 1 : imports of python lib
import base64
import re
import time
from datetime import datetime
# 2 : imports of odoo
import odoo
from odoo import Command, _, api, fields, models # ASCIIbetically ordered
from odoo.tools.safe_eval import safe_eval as eval
# 3 : imports from odoo addons
from odoo.addons.web.controllers.main import login_redirect
from odoo.addons.website.models.website import slug
اصطلاحات برنامهنویسی (پایتون)¶
همیشه خوانایی را بر اختصار یا استفاده از ویژگیها یا اصطلاحات زبان ترجیح دهید.
از
.clone()
استفاده نکنید
# bad
new_dict = my_dict.clone()
new_list = old_list.clone()
# good
new_dict = dict(my_dict)
new_list = list(old_list)
دایکشنری پایتون: ایجاد و بهروزرسانی
# -- creation empty dict
my_dict = {}
my_dict2 = dict()
# -- creation with values
# bad
my_dict = {}
my_dict['foo'] = 3
my_dict['bar'] = 4
# good
my_dict = {'foo': 3, 'bar': 4}
# -- update dict
# bad
my_dict['foo'] = 3
my_dict['bar'] = 4
my_dict['baz'] = 5
# good
my_dict.update(foo=3, bar=4, baz=5)
my_dict = dict(my_dict, **my_dict2)
از نامهای متغیر/کلاس/روش معنادار استفاده کنید
متغیر بیفایده: متغیرهای موقت میتوانند با نامگذاری اشیاء کد را واضحتر کنند، اما این به این معنا نیست که همیشه باید متغیرهای موقت ایجاد کنید:
# pointless
schema = kw['schema']
params = {'schema': schema}
# simpler
params = {'schema': kw['schema']}
چندین نقطه بازگشت قابل قبول هستند، زمانی که سادهتر باشند.
# a bit complex and with a redundant temp variable
def axes(self, axis):
axes = []
if type(axis) == type([]):
axes.extend(axis)
else:
axes.append(axis)
return axes
# clearer
def axes(self, axis):
if type(axis) == type([]):
return list(axis) # clone the axis
else:
return [axis] # single-element list
توابع داخلی را بشناسید: شما باید حداقل یک درک پایهای از تمام توابع داخلی پایتون داشته باشید (http://docs.python.org/library/functions.html)
value = my_dict.get('key', None) # very very redundant
value = my_dict.get('key') # good
همچنین، if 'key' in my_dict
و if my_dict.get('key')
معانی بسیار متفاوتی دارند، مطمئن شوید که از گزینه درست استفاده میکنید.
یادگیری لیست کامپریهنشنها: از لیست کامپریهنشن، دیکشنری کامپریهنشن و دستکاریهای پایه با استفاده از
map
،filter
،sum
و ... استفاده کنید. این کار کد را خواناتر میکند.
# not very good
cube = []
for i in res:
cube.append((i['id'],i['name']))
# better
cube = [(i['id'], i['name']) for i in res]
مجموعهها نیز بولی هستند: در پایتون، بسیاری از اشیاء هنگام ارزیابی در یک زمینه بولی (مانند if) دارای مقدار "شبیه به بولی" هستند. از جمله این اشیاء، مجموعهها (لیستها، دیکشنریها، مجموعهها و ...) هستند که هنگام خالی بودن "نادرست" و هنگام داشتن آیتمها "درست" محسوب میشوند.
bool([]) is False
bool([1]) is True
bool([False]) is True
بنابراین، میتوانید بنویسید if some_collection:
به جای if len(some_collection):
.
تکرار بر روی قابل تکرارها
# creates a temporary list and looks bar
for key in my_dict.keys():
"do something..."
# better
for key in my_dict:
"do something..."
# accessing the key,value pair
for key, value in my_dict.items():
"do something..."
از dict.setdefault استفاده کنید
# longer.. harder to read
values = {}
for element in iterable:
if element not in values:
values[element] = []
values[element].append(other_value)
# better.. use dict.setdefault method
values = {}
for element in iterable:
values.setdefault(element, []).append(other_value)
به عنوان یک توسعهدهنده خوب، کد خود را مستند کنید (توضیحات متنی برای متدها، نظرات ساده برای بخشهای پیچیده کد).
علاوه بر این دستورالعملها، ممکن است لینک زیر نیز برای شما جالب باشد: https://david.goodger.org/projects/pycon/2007/idiomatic/handout.html (کمی قدیمی است، اما بسیار مرتبط)
برنامهنویسی در اودو¶
از ایجاد ژنراتورها و دکوراتورها خودداری کنید: فقط از موارد ارائه شده توسط API اودو استفاده کنید.
مانند پایتون، از روشهای
filtered
،mapped
،sorted
و ... استفاده کنید تا خوانایی کد و عملکرد بهبود یابد.
انتشار زمینه¶
زمینه یک «frozendict» است که نمیتوان آن را تغییر داد. برای فراخوانی یک متد با زمینهای متفاوت، باید از متد «with_context» استفاده شود:
records.with_context(new_context).do_stuff() # all the context is replaced
records.with_context(**additionnal_context).do_other_stuff() # additionnal_context values override native context ones
هشدار
ارسال پارامتر در زمینه میتواند عوارض جانبی خطرناکی داشته باشد.
از آنجا که مقادیر به صورت خودکار منتقل میشوند، ممکن است برخی رفتارهای غیرمنتظره ظاهر شوند. فراخوانی متد create()
یک مدل با کلید default_my_field در زمینه (context)، مقدار پیشفرض my_field را برای مدل مربوطه تنظیم میکند. اما اگر در طول این ایجاد، اشیاء دیگری (مانند sale.order.line در زمان ایجاد sale.order) که دارای فیلدی به نام my_field هستند ایجاد شوند، مقدار پیشفرض آنها نیز تنظیم خواهد شد.
اگر نیاز دارید یک کلید زمینه ایجاد کنید که بر رفتار یک شی تأثیر بگذارد، یک نام مناسب انتخاب کنید و در نهایت آن را با نام ماژول پیشوند کنید تا تأثیر آن جدا شود. یک مثال خوب کلیدهای ماژول «mail» هستند: mail_create_nosubscribe، mail_notrack، mail_notify_user_signature، ...
به توسعهپذیری فکر کنید¶
توابع و روشها نباید شامل منطق زیادی باشند: داشتن تعداد زیادی روش کوچک و ساده بهتر از داشتن تعداد کمی روش بزرگ و پیچیده است. یک قانون کلی خوب این است که به محض اینکه یک روش بیش از یک مسئولیت داشته باشد، آن را تقسیم کنید (به اصل مسئولیت واحد در http://en.wikipedia.org/wiki/Single_responsibility_principle مراجعه کنید).
سختکدنویسی منطق کسبوکار در یک متد باید اجتناب شود زیرا مانع از گسترش آسان توسط زیرماژول میشود.
# do not do this
# modifying the domain or criteria implies overriding whole method
def action(self):
... # long method
partners = self.env['res.partner'].search(complex_domain)
emails = partners.filtered(lambda r: arbitrary_criteria).mapped('email')
# better but do not do this either
# modifying the logic forces to duplicate some parts of the code
def action(self):
...
partners = self._get_partners()
emails = partners._get_emails()
# better
# minimum override
def action(self):
...
partners = self.env['res.partner'].search(self._get_partner_domain())
emails = partners.filtered(lambda r: r._filter_partners()).mapped('email')
کد بالا برای مثال بیش از حد قابل گسترش است، اما باید خوانایی آن در نظر گرفته شود و یک توازن برقرار گردد.
همچنین، توابع خود را بهطور مناسب نامگذاری کنید: توابع کوچک و با نامگذاری مناسب نقطه شروع کد خوانا/قابل نگهداری و مستندات دقیقتر هستند.
این توصیه همچنین برای کلاسها، فایلها، ماژولها و بستهها نیز مرتبط است. (همچنین ببینید http://en.wikipedia.org/wiki/Cyclomatic_complexity)
هرگز تراکنش را ثبت نکنید¶
چارچوب اودو مسئول فراهم کردن زمینه تراکنشی برای تمامی فراخوانیهای RPC است. اصل بر این است که یک کرسر جدید پایگاه داده در ابتدای هر فراخوانی RPC باز میشود و زمانی که فراخوانی به پایان رسید، درست قبل از ارسال پاسخ به کلاینت RPC، تعهد داده میشود، تقریباً به این صورت:
def execute(self, db_name, uid, obj, method, *args, **kw):
db, pool = pooler.get_db_and_pool(db_name)
# create transaction cursor
cr = db.cursor()
try:
res = pool.execute_cr(cr, uid, obj, method, *args, **kw)
cr.commit() # all good, we commit
except Exception:
cr.rollback() # error, rollback everything atomically
raise
finally:
cr.close() # always close cursor opened manually
return res
اگر هر خطایی در حین اجرای فراخوانی RPC رخ دهد، تراکنش به صورت اتمی بازگردانده میشود و وضعیت سیستم حفظ میگردد.
به همین ترتیب، سیستم همچنین یک تراکنش اختصاصی در طول اجرای مجموعههای آزمایشی ارائه میدهد، بنابراین بسته به گزینههای راهاندازی سرور، میتوان آن را بازگرداند یا نه.
پیامد این است که اگر به صورت دستی cr.commit()
را در هر جایی فراخوانی کنید، احتمال بسیار زیادی وجود دارد که سیستم را به روشهای مختلف خراب کنید، زیرا باعث تعهدات جزئی و در نتیجه بازگشتهای جزئی و نامرتب خواهید شد که منجر به موارد زیر میشود:
دادههای کسبوکار ناسازگار، معمولاً از دست دادن دادهها
ناهماهنگی جریان کار، اسناد بهطور دائمی گیر کردهاند
آزمایشهایی که نمیتوانند بهطور کامل بازگردانده شوند و شروع به آلوده کردن پایگاه داده و ایجاد خطا میکنند (این موضوع حتی اگر هیچ خطایی در طول تراکنش رخ ندهد نیز صادق است).
- در اینجا یک قانون بسیار ساده وجود دارد:
شما هرگز نباید خودتان
cr.commit()
را فراخوانی کنید، مگر اینکه بهطور صریح یک کرسر پایگاه داده خودتان را ایجاد کرده باشید! و شرایطی که نیاز به این کار دارید، استثنایی است!و اگر به هر حال خودتان یک نشانگر ایجاد کردهاید، باید موارد خطا و بازگردانی صحیح را مدیریت کنید، همچنین نشانگر را بهدرستی ببندید زمانی که کارتان با آن تمام شد.
و برخلاف باور عمومی، حتی در شرایط زیر نیازی به فراخوانی cr.commit()
ندارید:
- در متد _auto_init()
یک شیء models.Model: این مورد توسط متد اولیهسازی افزونهها یا تراکنش ORM هنگام ایجاد مدلهای سفارشی مدیریت میشود.
- در گزارشها: commit()
نیز توسط فریمورک مدیریت میشود، بنابراین میتوانید حتی از داخل یک گزارش پایگاه داده را بهروزرسانی کنید.
- در متدهای models.Transient: این متدها دقیقاً مانند متدهای معمولی models.Model فراخوانی میشوند، در یک تراکنش و با cr.commit()/rollback()
مربوطه در انتها.
- و غیره. (در صورت شک، به قانون کلی بالا مراجعه کنید!)
از این پس، تمام فراخوانیهای cr.commit()
خارج از چارچوب سرور باید دارای توضیح صریح باشند که توضیح دهد چرا این فراخوانیها کاملاً ضروری هستند، چرا واقعاً صحیح هستند، و چرا تراکنشها را مختل نمیکنند. در غیر این صورت، ممکن است حذف شوند!
از روش ترجمه به درستی استفاده کنید¶
اودو از روشی مشابه GetText به نام "underscore" _()
استفاده میکند تا نشان دهد که یک رشته ثابت استفادهشده در کد نیاز به ترجمه در زمان اجرا دارد. این روش با استفاده از زبان محیط در self.env._
در دسترس است.
برای استفاده از آن و جلوگیری از پر شدن ترجمهها با مطالب بیفایده، باید چند قانون بسیار مهم رعایت شود.
به طور کلی، این روش باید فقط برای رشتههای ثابت که به صورت دستی در کد نوشته شدهاند استفاده شود و برای ترجمه مقادیر فیلدها، مانند نام محصولات و غیره، کار نخواهد کرد. برای این کار باید از علامت ترجمه در فیلد مربوطه استفاده شود.
این روش پارامترهای اختیاری موقعیتی یا نامگذاری شده را میپذیرد. قانون بسیار ساده است: فراخوانیهای روش آندرلاین باید همیشه به صورت self.env._('literal string')
و هیچ چیز دیگری باشد:
_ = self.env._
# good: plain strings
error = _('This record is locked!')
# good: strings with formatting patterns included
error = _('Record %s cannot be modified!', record)
# ok too: multi-line literal strings
error = _("""This is a bad multiline example
about record %s!""", record)
error = _('Record %s cannot be modified' \
'after being validated!', record)
# bad: tries to translate after string formatting
# (pay attention to brackets!)
# This does NOT work and messes up the translations!
error = _('Record %s cannot be modified!' % record)
# bad: formatting outside of translation
# This won't benefit from fallback mechanism in case of bad translation
error = _('Record %s cannot be modified!') % record
# bad: dynamic string, string concatenation, etc are forbidden!
# This does NOT work and messes up the translations!
error = _("'" + que_rec['question'] + "' \n")
# bad: field values are automatically translated by the framework
# This is useless and will not work the way you think:
error = _("Product %s is out of stock!") % _(product.name)
# and the following will of course not work as already explained:
error = _("Product %s is out of stock!" % product.name)
# Instead you can do the following and everything will be translated,
# including the product name if its field definition has the
# translate flag properly set:
error = _("Product %s is not available!", product.name)
همچنین به خاطر داشته باشید که مترجمان باید با مقادیر واقعی که به تابع underscore ارسال میشوند کار کنند، بنابراین لطفاً سعی کنید آنها را قابل فهم کنید و از کاراکترهای اضافی و قالببندی غیرضروری خودداری کنید. مترجمان باید آگاه باشند که الگوهای قالببندی مانند %s
یا %d
، خطوط جدید و غیره باید حفظ شوند، اما مهم است که از این موارد به شیوهای منطقی و واضح استفاده شود.
# Bad: makes the translations hard to work with
error = "'" + question + _("' \nPlease enter an integer value ")
# Ok (pay attention to position of the brackets too!)
error = _("Answer to question %s is not valid.\n" \
"Please enter an integer value.", question)
# Better
error = _("Answer to question %(title)s is not valid.\n" \
"Please enter an integer value.", title=question)
به طور کلی در Odoo، هنگام کار با رشتهها، ترجیحاً از %
به جای .format()
(زمانی که فقط یک متغیر برای جایگزینی در رشته وجود دارد) استفاده کنید و ترجیحاً از %(varname)
به جای موقعیت (زمانی که چندین متغیر باید جایگزین شوند) استفاده کنید. این کار ترجمه را برای مترجمان جامعه آسانتر میکند.
نمادها و قراردادها¶
- نام مدل (با استفاده از نشانهگذاری نقطهای، با پیشوند نام ماژول):
هنگام تعریف یک مدل در Odoo: از فرم مفرد نام استفاده کنید (res.partner و sale.order به جای res.partnerS و saleS.orderS)
هنگام تعریف یک Odoo Transient (ویزارد): از
<related_base_model>.<action>
استفاده کنید، که در آن related_base_model مدل پایه (تعریفشده در models/) مرتبط با transient است و action نام کوتاهی از عملکرد transient است. از استفاده از کلمه wizard خودداری کنید. به عنوان مثال:account.invoice.make
،project.task.delegate.batch
، ...هنگام تعریف مدل گزارش (مانند نماهای SQL): از
<related_base_model>.report.<action>
بر اساس کنوانسیون Transient استفاده کنید.
کلاس پایتون اودو: از حالت CamelCase (سبک شیءگرایی) استفاده کنید.
class AccountInvoice(models.Model):
...
- نام متغیر :
از نامگذاری CamelCase برای متغیر مدل استفاده کنید
از نامگذاری با حروف کوچک و خط زیر برای متغیرهای عمومی استفاده کنید.
نام متغیر خود را با _id یا _ids پسوند کنید اگر شامل شناسه رکورد یا لیستی از شناسهها باشد. از
partner_id
برای نگهداری یک رکورد از res.partner استفاده نکنید.
Partner = self.env['res.partner']
partners = Partner.browse(ids)
partner_id = partners[0].id
فیلدهای
One2Many
وMany2Many
همیشه باید پسوند _ids داشته باشند (مثال: sale_order_line_ids)فیلدهای
Many2One
باید پسوند _id داشته باشند (مثال: partner_id، user_id، ...)- قراردادهای روش
محاسبه فیلد: الگوی روش محاسبه به صورت _compute_<field_name> است.
روش جستجو: الگوی روش جستجو به صورت _search_<field_name> است
روش پیشفرض: الگوی روش پیشفرض به صورت _default_<field_name> است
روش انتخاب: الگوی روش انتخاب به صورت _selection_<field_name> است
متد تغییر: الگوی متد تغییر به صورت _onchange_<field_name> میباشد.
روش محدودیت: الگوی روش محدودیت _check_<constraint_name>
روش اقدام: یک روش اقدام شیء با action_ پیشوند میشود. از آنجا که فقط از یک رکورد استفاده میکند،
self.ensure_one()
را در ابتدای روش اضافه کنید.
- در یک ویژگی مدل، ترتیب باید اینگونه باشد
ویژگیهای خصوصی (
_name
،_description
،_inherit
،_sql_constraints
، ...)روش پیشفرض و
default_get
اعلامیههای فیلد
محاسبه، معکوس و روشهای جستجو به همان ترتیبی که فیلد اعلام شده است.
روش انتخاب (روشهایی که برای بازگرداندن مقادیر محاسبهشده برای فیلدهای انتخاب استفاده میشوند)
روشهای محدودیت (
@api.constrains
) و روشهای تغییر (@api.onchange
)روشهای CRUD (بازنویسی ORM)
روشهای اقدام
و در نهایت، سایر روشهای کسبوکار.
class Event(models.Model):
# Private attributes
_name = 'event.event'
_description = 'Event'
# Default methods
def _default_name(self):
...
# Fields declaration
name = fields.Char(string='Name', default=_default_name)
seats_reserved = fields.Integer(string='Reserved Seats', store=True
readonly=True, compute='_compute_seats')
seats_available = fields.Integer(string='Available Seats', store=True
readonly=True, compute='_compute_seats')
price = fields.Integer(string='Price')
event_type = fields.Selection(string="Type", selection='_selection_type')
# compute and search fields, in the same order of fields declaration
@api.depends('seats_max', 'registration_ids.state', 'registration_ids.nb_register')
def _compute_seats(self):
...
@api.model
def _selection_type(self):
return []
# Constraints and onchanges
@api.constrains('seats_max', 'seats_available')
def _check_seats_limit(self):
...
@api.onchange('date_begin')
def _onchange_date_begin(self):
...
# CRUD methods (and name_search, _search, ...) overrides
def create(self, values):
...
# Action methods
def action_validate(self):
self.ensure_one()
...
# Business methods
def mail_user_confirm(self):
...
جاوااسکریپت¶
سازماندهی فایلهای ثابت¶
افزونههای Odoo دارای برخی قواعد برای ساختاردهی فایلهای مختلف هستند. در اینجا توضیحات بیشتری در مورد نحوه سازماندهی داراییهای وب ارائه میدهیم.
اولین چیزی که باید بدانید این است که سرور Odoo به صورت ایستا تمام فایلهایی که در پوشه static/ قرار دارند را ارائه میدهد، اما با پیشوند نام افزونه. بنابراین، به عنوان مثال، اگر فایلی در مسیر addons/web/static/src/js/some_file.js قرار داشته باشد، به صورت ایستا در آدرس your-odoo-server.com/web/static/src/js/some_file.js در دسترس خواهد بود.
روال معمول این است که کد را بر اساس ساختار زیر سازماندهی کنید:
استاتیک: تمام فایلهای استاتیک به طور کلی
static/lib: اینجا مکانی است که کتابخانههای جاوا اسکریپت باید در یک زیر پوشه قرار گیرند. بنابراین، به عنوان مثال، تمام فایلهای کتابخانه jquery در مسیر addons/web/static/lib/jquery قرار دارند.
static/src: پوشه کد منبع استاتیک عمومی
static/src/css: تمام فایلهای CSS
فونتهای ایستا
ایستا/تصویر
static/src/js
static/src/js/tours: فایلهای راهنمای کاربر نهایی (آموزشها، نه تستها)
static/src/scss: فایلهای scss
static/src/xml: تمام قالبهای qweb که در JS رندر خواهند شد
static/tests: اینجا جایی است که تمام فایلهای مربوط به آزمونها قرار میگیرند.
static/tests/tours: اینجا جایی است که تمام فایلهای تست تور (نه آموزشها) را قرار میدهیم.
راهنمای کدنویسی جاوااسکریپت¶
use strict;
برای تمامی فایلهای جاوااسکریپت توصیه میشود.از یک لینتر استفاده کنید (jshint، ...)
هرگز کتابخانههای جاوااسکریپت کوچکشده را اضافه نکنید
از حروف بزرگ و کوچک ترکیبی برای اعلام کلاس استفاده کنید
راهنمای دقیقتر جاوااسکریپت در ویکی گیتهاب توضیح داده شده است. همچنین میتوانید با مشاهده مراجع جاوااسکریپت، به APIهای موجود در جاوااسکریپت نگاهی بیندازید.
CSS و SCSS¶
نحو و قالببندی¶
.o_foo, .o_foo_bar, .o_baz {
height: $o-statusbar-height;
.o_qux {
height: $o-statusbar-height * 0.5;
}
}
.o_corge {
background: $o-list-footer-bg-color;
}
.o_foo, .o_foo_bar, .o_baz {
height: 32px;
}
.o_foo .o_quux, .o_foo_bar .o_quux, .o_baz .o_qux {
height: 16px;
}
.o_corge {
background: #EAEAEA;
}
چهار (۴) فاصله تورفتگی، بدون استفاده از تب؛
ستونها با حداکثر عرض ۸۰ کاراکتر؛
آکولاد باز (
{
): فضای خالی بعد از آخرین انتخابگر؛آکولاد بسته (
}
): در یک خط جدید؛یک خط برای هر اعلان؛
استفاده معنادار از فضای خالی.
"stylelint.config": {
"rules": {
// https://stylelint.io/user-guide/rules
// Avoid errors
"block-no-empty": true,
"shorthand-property-no-redundant-values": true,
"declaration-block-no-shorthand-property-overrides": true,
// Stylistic conventions
"indentation": 4,
"function-comma-space-after": "always",
"function-parentheses-space-inside": "never",
"function-whitespace-after": "always",
"unit-case": "lower",
"value-list-comma-space-after": "always-single-line",
"declaration-bang-space-after": "never",
"declaration-bang-space-before": "always",
"declaration-colon-space-after": "always",
"declaration-colon-space-before": "never",
"block-closing-brace-empty-line-before": "never",
"block-opening-brace-space-before": "always",
"selector-attribute-brackets-space-inside": "never",
"selector-list-comma-space-after": "always-single-line",
"selector-list-comma-space-before": "never-single-line",
}
},
ترتیب ویژگیها¶
ویژگیهای سفارش را از "بیرون" به داخل مرتب کنید، از position
شروع کرده و با قوانین تزئینی (font
, filter
و غیره) پایان دهید.
متغیرهای SCSS محدود و متغیرهای CSS باید در بالاترین قسمت قرار گیرند و یک خط خالی آنها را از سایر اعلانها جدا کند.
.o_element {
$-inner-gap: $border-width + $legend-margin-bottom;
--element-margin: 1rem;
--element-size: 3rem;
@include o-position-absolute(1rem);
display: block;
margin: var(--element-margin);
width: calc(var(--element-size) + #{$-inner-gap});
border: 0;
padding: 1rem;
background: blue;
font-size: 1rem;
filter: blur(2px);
}
قراردادهای نامگذاری¶
قواعد نامگذاری در CSS بسیار مفید هستند و کد شما را دقیقتر، شفافتر و اطلاعاتیتر میکنند.
id
اجتناب کنید و کلاسهای خود را با پیشوند o_<module_name>
آغاز کنید، که در آن <module_name>
نام فنی ماژول (مانند sale
, im_chat
, ...) یا مسیر اصلی رزرو شده توسط ماژول (عمدتاً برای ماژولهای وبسایت، مانند: o_forum
برای ماژول website_forum
) است.o_
استفاده میکند.از ایجاد کلاسها و نامهای متغیر بیش از حد خاص خودداری کنید. هنگام نامگذاری عناصر تو در تو، روش "نوه" را انتخاب کنید.
Example
نکن
<div class=“o_element_wrapper”>
<div class=“o_element_wrapper_entries”>
<span class=“o_element_wrapper_entries_entry”>
<a class=“o_element_wrapper_entries_entry_link”>Entry</a>
</span>
</div>
</div>
انجام بده
<div class=“o_element_wrapper”>
<div class=“o_element_entries”>
<span class=“o_element_entry”>
<a class=“o_element_link”>Entry</a>
</span>
</div>
</div>
علاوه بر فشردهتر بودن، این روش نگهداری را آسانتر میکند زیرا نیاز به تغییر نام هنگام وقوع تغییرات در DOM را محدود میکند.
متغیرهای SCSS¶
قانون استاندارد ما $o-[root]-[element]-[property]-[modifier]
است، با:
$o-
پیشوند.
[ریشه]
یا نام مؤلفه یا نام ماژول (مؤلفهها اولویت دارند).
[عنصر]
یک شناسه اختیاری برای عناصر داخلی.
[ویژگی]
ویژگی/رفتاری که توسط متغیر تعریف شده است.
[تغییر دهنده]
یک اصلاحکننده اختیاری.
Example
$o-block-color: value;
$o-block-title-color: value;
$o-block-title-color-hover: value;
متغیرهای SCSS (محدودهدار)¶
این متغیرها در داخل بلوکها تعریف شدهاند و از بیرون قابل دسترسی نیستند. قرارداد استاندارد ما $-[نام متغیر]
است.
Example
.o_element {
$-inner-gap: compute-something;
margin-right: $-inner-gap;
.o_element_child {
margin-right: $-inner-gap * 0.5;
}
}
همچنین ببینید
میکسینها و توابع SCSS¶
قرارداد استاندارد ما o-[name]
است. از نامهای توصیفی استفاده کنید. هنگام نامگذاری توابع، از افعال به صورت امری استفاده کنید (مثلاً: get
، make
، apply
...).
نام آرگومانهای اختیاری را در فرم متغیرهای محدود مشخص کنید، به صورت $-[argument]
.
Example
@mixin o-avatar($-size: 1.5em, $-radius: 100%) {
width: $-size;
height: $-size;
border-radius: $-radius;
}
@function o-invert-color($-color, $-amount: 100%) {
$-inverse: change-color($-color, $-hue: hue($-color) + 180);
@return mix($-inverse, $-color, $-amount);
}
همچنین ببینید
میکسینها در مستندات SASS <https://sass-lang.com/documentation/at-rules/mixin>`_
متغیرهای CSS¶
در Odoo، استفاده از متغیرهای CSS به طور خاص مربوط به DOM است. از آنها برای سازگاری متنی طراحی و چیدمان استفاده کنید.
قرارداد استاندارد ما BEM است، بنابراین --[ریشه]__[عنصر]-[ویژگی]--[تغییردهنده]
، با:
[ریشه]
یا نام مؤلفه یا نام ماژول (مؤلفهها اولویت دارند).
[عنصر]
یک شناسه اختیاری برای عناصر داخلی.
[ویژگی]
ویژگی/رفتاری که توسط متغیر تعریف شده است.
[تغییر دهنده]
یک اصلاحکننده اختیاری.
Example
.o_kanban_record {
--KanbanRecord-width: value;
--KanbanRecord__picture-border: value;
--KanbanRecord__picture-border--active: value;
}
// Adapt the component when rendered in another context.
.o_form_view {
--KanbanRecord-width: another-value;
--KanbanRecord__picture-border: another-value;
--KanbanRecord__picture-border--active: another-value;
}
استفاده از متغیرهای CSS¶
در Odoo، استفاده از متغیرهای CSS به طور خاص به DOM مرتبط است، به این معنا که برای تطبیق متنی طراحی و چیدمان استفاده میشود و نه برای مدیریت سیستم طراحی جهانی. این متغیرها معمولاً زمانی استفاده میشوند که ویژگیهای یک مؤلفه میتوانند در زمینههای خاص یا شرایط دیگر تغییر کنند.
ما این ویژگیها را داخل بلوک اصلی مؤلفه تعریف میکنیم و مقادیر پیشفرض را ارائه میدهیم.
Example
my_component.scss
¶.o_MyComponent {
color: var(--MyComponent-color, #313131);
}
my_dashboard.scss
¶.o_MyDashboard {
// Adapt the component in this context only
--MyComponent-color: #017e84;
}
همچنین ببینید
متغیرهای CSS در مستندات وب MDN <https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties>`_
متغیرهای CSS و SCSS¶
علیرغم شباهت ظاهری، متغیرهای CSS
و SCSS
رفتار بسیار متفاوتی دارند. تفاوت اصلی این است که متغیرهای SCSS
دستوری هستند و در زمان کامپایل حذف میشوند، در حالی که متغیرهای CSS
اعلانی هستند و در خروجی نهایی گنجانده میشوند.
همچنین ببینید
تفاوت متغیرهای CSS/SCSS در مستندات SASS <https://sass-lang.com/documentation/variables#:~:text=CSS%20variables%20are%20included%20in,use%20will%20stay%20the%20same>`_
در اودو، ما بهترینهای هر دو جهان را ترکیب میکنیم: استفاده از متغیرهای SCSS
برای تعریف سیستم طراحی و انتخاب متغیرهای CSS
برای تطبیقهای متنی.
اجرای مثال قبلی باید با افزودن متغیرهای SCSS بهبود یابد تا کنترل در سطح بالا به دست آید و هماهنگی با سایر اجزا تضمین شود.
کلاس شبه :root
¶
تعریف متغیرهای CSS بر روی شبهکلاس :root
تکنیکی است که معمولاً در رابط کاربری Odoo استفاده نمیکنیم. این روش معمولاً برای دسترسی و تغییر متغیرهای CSS به صورت جهانی استفاده میشود. ما این کار را به جای آن با استفاده از SCSS انجام میدهیم.
استثناهای این قانون باید کاملاً واضح باشند، مانند قالبهایی که در بستههای مختلف به اشتراک گذاشته شدهاند و نیاز به سطح مشخصی از آگاهی زمینهای دارند تا بهدرستی نمایش داده شوند.