13 Commits
16 changed files with 252 additions and 7 deletions
+4 -1
View File
@@ -1,6 +1,6 @@
{
'name': '教育',
'version': '18.0.3.0.4',
'version': '18.0.3.1.0',
'summary': 'Odoo 18 教学管理平台 — 师生管理 + 课程管理 + 作业/进展(老师·学生双角色)',
'description': """
Odoo 18 教学管理平台(面向老师与学生,双角色权限)
@@ -59,8 +59,11 @@
'web.assets_backend': [
'college/static/src/scss/college_theme.scss',
'college/static/src/js/hide_company_menu.js',
'college/static/src/js/hide_user_menu_items.js',
'college/static/src/js/hide_activity_menu.js',
'college/static/src/js/portal_indicator.js',
'college/static/src/xml/portal_indicator.xml',
'college/static/src/xml/sidenav_tooltip.xml',
],
'web.assets_web_dark': [
'college/static/src/scss/college_theme_dark.scss',
+3 -1
View File
@@ -23,7 +23,9 @@
23=Fleet(车辆)
如需保留/恢复某类目,从下面列表中删除对应 id 即可(注意总数会变化)。 -->
<field name="domain">[('application', '=', True), ('category_id', 'not in', [13, 54, 71, 15, 94, 6, 10, 11, 20, 33, 55, 37, 59, 21, 89, 90, 92, 103, 28, 32, 88, 23])]</field>
<field name="context">{}</field>
<!-- shixun_hide_empty_categories:搜索面板「类别」列过滤掉计数为 0 的类别
(见 models/ir_module_module_info.py 对 search_panel_select_range 的包装) -->
<field name="context">{'shixun_hide_empty_categories': True}</field>
<field name="search_view_id" ref="shixun_module_search_view"/>
<field name="limit">50</field>
</record>
+16
View File
@@ -20,6 +20,13 @@ class SchoolAssignment(models.Model):
ondelete='cascade',
domain="[('state', '=', 'published'), ('teacher_id', '=', uid)]",
help='只能选择自己已发布的课程。')
chapter_id = fields.Many2one(
'yuthon.school.course.chapter',
string='对应章节',
domain="[('course_id', '=', course_id)]",
ondelete='set null',
index=True,
help='该作业所属的课程章节,不选则代表整门课程作业。')
teacher_id = fields.Many2one(
'res.users', string='布置教师',
default=lambda self: self.env.user, required=True)
@@ -111,6 +118,15 @@ class SchoolAssignmentSubmission(models.Model):
_order = 'submit_time desc, id'
_rec_name = 'name'
chapter_id = fields.Many2one(
'yuthon.school.course.chapter',
string='章节',
related='assignment_id.chapter_id',
store=True, index=True,
)
name = fields.Char(string='提交标题', readonly=True, copy=False)
assignment_id = fields.Many2one(
'yuthon.school.assignment', string='作业', required=True, ondelete='cascade')
+14
View File
@@ -106,6 +106,20 @@ class SchoolCourse(models.Model):
chapter_count = fields.Integer(
string='章节数', compute='_compute_chapter_count', store=True)
# 课程作业:一门课对应多条作业记录
assignment_ids = fields.One2many(
'yuthon.school.assignment',
'course_id',
string='作业布置',
)
# 学习进展:一门课对应多条学员进度记录
progress_ids = fields.One2many(
'yuthon.school.progress',
'course_id',
string='学习进展',
)
@api.depends('student_ids')
def _compute_student_count(self):
for record in self:
+15
View File
@@ -48,6 +48,21 @@ class IrModuleModule(models.Model):
help='在「模块信息 / 知识点」页展示的核心概念与使用要点(由 college 维护)',
)
@api.model
def search_panel_select_range(self, field_name, **kwargs):
"""搜索面板类别列:按 context 开关过滤掉计数为 0 的顶层类别。
背景:base 对 ir.module.module 的 search_panel_select_range 做了重写
(ir_module.py),只要顶层类别的子树里存在任何模块就会返回该类别,
即使它名下没有一条记录匹配当前搜索域(计数为 0、界面上不显示数字)。
「实训模块」动作通过 context 开关 shixun_hide_empty_categories 启用本过滤,
不影响原生「应用」页面;hierarchize/expand 等视图属性在该重写分支中不生效。
"""
res = super().search_panel_select_range(field_name, **kwargs)
if field_name == 'category_id' and self.env.context.get('shixun_hide_empty_categories'):
res['values'] = [v for v in res.get('values', []) if v.get('__count')]
return res
@api.model
def populate_module_info(self):
"""为 school 自建模块写入「功能亮点 / 模块介绍」文案。
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -0,0 +1,18 @@
/**
* 隐藏导航栏右侧的「活动」图标(日历+时钟那个,点击弹出"我的活动"下拉)
*
* 原理:导航栏右侧的系统托盘 (systray) 是一个组件注册表,
* mail 模块在加载时把活动菜单注册了进去(见
* odoo18/addons/mail/static/src/core/web/activity_menu.js 最后一行
* registry.category("systray").add("mail.activity_menu", ...))。
* 这里在资源包加载阶段把它从注册表里摘掉,导航栏就不会渲染它。
*
* 如需恢复:删除本文件并把 __manifest__.py assets 里对应的一行去掉即可。
*
* 注意:本文件改动需要「升级模块」才会生效(-d <库名> -u college),
* 并强刷浏览器 (Ctrl+Shift+R)。
*/
/** @odoo-module **/
import { registry } from "@web/core/registry";
registry.category("systray").remove("mail.activity_menu");
@@ -0,0 +1,57 @@
/**
* 右上角头像下拉菜单:只保留「退出登录」
*
* 原理:Odoo 前端把头像菜单的每个条目注册到
* registry.category("user_menuitems") 里(web / web_tour / hr /
* yuthon_base 的 udoo_om_ux、quick_navigation 等模块都会往里加)。
* 这里在资源包加载阶段把白名单之外的条目全部摘掉,并监听 UPDATE 事件,
* 保证晚于本文件注册的条目(例如以后新装的模块)也会被自动移除,
* 因此不用关心资源加载顺序。
*
* 被移除的条目一览(想恢复哪个,把它的 key 加进 KEEP 即可):
* documentation 文档 (web)
* support 支持 (web)
* shortcuts 快捷键 (web)
* profile 偏好 (web / hr)
* odoo_account Odoo.com 账户 (web)
* install_pwa 安装应用 (web)
* separator 分隔线 (web)
* web_tour.tour_enabled 新手上路 (web_tour)
* appearance 外观 (udoo_om_ux)
* separator42 分隔线 (udoo_om_ux)
* quick_menu_config 快捷菜单配置 (quick_navigation)
*
* 注意:本文件改动需要「升级模块 + 重启」才会生效
* (-d <库名> -u college),并强刷浏览器 (Ctrl+Shift+R)。
*/
/** @odoo-module **/
import { registry } from "@web/core/registry";
const KEEP = new Set(["log_out"]);
const userMenuRegistry = registry.category("user_menuitems");
/**
* 移除所有不在白名单里的菜单项。
* 用 getEntries() 先取快照再删,避免边遍历边删。
*/
function pruneUserMenu() {
const keys = userMenuRegistry.getEntries().map(([key]) => key);
for (const key of keys) {
if (!KEEP.has(key)) {
userMenuRegistry.remove(key);
}
}
}
// 1) 清理已经注册过的条目
pruneUserMenu();
// 2) 监听后续新增:只处理 add,不处理 delete,避免自己触发自己造成递归。
// 注意:Owl 的 EventBus 继承自 EventTarget,trigger 用 CustomEvent 派发,
// 监听函数收到的是事件对象,注册表传的 payload 在 ev.detail 里。
userMenuRegistry.addEventListener("UPDATE", (ev) => {
const { operation, key } = ev.detail || {};
if (operation === "add" && !KEEP.has(key)) {
userMenuRegistry.remove(key);
}
});
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
侧边栏窄模式悬浮提示
背景:udoo_om_ux 左侧导航栏(.o_sidenav)拖窄 / 切换窄模式(宽度 < 142 时加 .sm)
后,菜单文字 .nav-title 被 display:none 隐藏、只剩图标,而原生 title 属性恰好在
被隐藏的 span 上,导致鼠标悬停看不到任何菜单名提示。
做法:继承 wub.NavBar.AppsMenu,给每个菜单外壳 div.nav-item 挂 Odoo 原生
data-tooltip(黑色气泡,与系统其它提示一致),只在窄模式输出。
⚠ 为什么必须挂在外壳 div 上,而不是里面的 DropdownItem(踩过的坑):
Odoo 在开发者模式(URL 带 ?debug=1,debug 值会存进 session)下,模板继承的
applyInheritance() 会给每个被修改的元素「前面」插入一个调试注释节点
(见 odoo/addons/web/static/src/core/template_inheritance.js:260)。
- 如果改的是 .nav-item 的子元素,这个注释就成了 .nav-item 的 firstChild;
而主题 OmuxNavBar.preSubnavTiming 里写死了
evTarget.firstChild.classList.contains('on')
注释节点没有 classList → 报
TypeError: Cannot read properties of undefined (reading 'contains')
- 改外壳后,注释落在 .sidenav_cont 里当兄弟节点:主题其它地方用的是
element.children[...](只数元素,忽略注释),完全不受影响,悬停范围还是整行。
想改气泡方向把下面的 'right' 改成 'bottom' 即可。
-->
<templates xml:space="preserve">
<t t-inherit="wub.NavBar.AppsMenu" t-inherit-mode="extension">
<!-- 应用列表项(t-foreach 渲染出来的那一批) -->
<xpath expr="//div[hasclass('nav-item')][@t-foreach]" position="attributes">
<attribute name="t-att-data-tooltip">ue.sidenavWidth &lt; 142 ? (app.label or app.name) : false</attribute>
<attribute name="data-tooltip-position">right</attribute>
</xpath>
<!-- 收藏夹里「当前应用不在收藏中」时额外显示的那一项 -->
<xpath expr="//div[hasclass('nav-item')][@t-if][not(@t-foreach)]" position="attributes">
<attribute name="t-att-data-tooltip">ue.sidenavWidth &lt; 142 ? (currentApp.label or currentApp.name) : false</attribute>
<attribute name="data-tooltip-position">right</attribute>
</xpath>
</t>
</templates>
+2 -1
View File
@@ -7,7 +7,8 @@
<field name="name">yuthon.about.page.form</field>
<field name="model">yuthon.about.page</field>
<field name="arch" type="xml">
<form class="o_about_full_form">
<!-- create/edit 关掉:关于页内容由代码预置维护,页面右上角不再显示「创建/编辑」按钮 -->
<form class="o_about_full_form" create="false" edit="false">
<field name="content" widget="html" readonly="1" class="o_about_full"/>
</form>
</field>
+19 -2
View File
@@ -33,8 +33,8 @@
invisible="[('state', '!=', 'published')]"/>
<button name="action_to_draft" type="object" string="转为草稿"
invisible="[('state', '=', 'draft')]"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,published,archived"/>
<!-- <field name="state" widget="statusbar"-->
<!-- statusbar_visible="draft,published,archived"/>-->
</header>
<sheet>
<group>
@@ -104,6 +104,23 @@
<page string="业务情景">
<field name="business_scenario"/>
</page>
<page string="学习进度" groups="college.group_teacher,base.group_system">
<field name="progress_ids" mode="kanban,list" readonly="1"
context="{'kanban_view_ref': 'college.yuthon_school_progress_view_kanban', 'tree_view_ref': 'college.yuthon_school_progress_view_list'}"/>
</page>
<page string="作业布置" groups="college.group_teacher,base.group_system">
<field name="assignment_ids">
<list>
<field name="name" string="作业标题"/>
<field name="chapter_id" string="对应章节"/>
<field name="due_date" string="截止日期"/>
<field name="submission_count" string="提交数"/>
<field name="state" string="状态"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
+8 -2
View File
@@ -44,9 +44,13 @@
<field name="name" placeholder="请输入作业标题"/>
</h1>
</div>
<group>
<group>
<field name="course_id"/>
<field name="chapter_id"
domain="[('course_id', '=', course_id)]"
context="{'default_course_id': course_id}"/>
<field name="teacher_id" readonly="1"/>
<field name="due_date"/>
</group>
@@ -55,6 +59,7 @@
<field name="attachment_filename" invisible="1"/>
</group>
</group>
<notebook>
<page string="作业说明">
<field name="description"/>
@@ -119,7 +124,7 @@
<field name="name">yuthon.school.assignment.submission.list</field>
<field name="model">yuthon.school.assignment.submission</field>
<field name="arch" type="xml">
<list string="作业提交" multi_edit="1">
<list string="作业提交" multi_edit="1" create="0">
<header>
<button name="action_open_batch_wizard"
type="object" string="批量批改"
@@ -146,7 +151,8 @@
<field name="arch" type="xml">
<kanban default_group_by="state"
default_order="due_date desc, id"
class="college-course-kanban college-kanban-homework o_kanban_mobile">
class="college-course-kanban college-kanban-homework o_kanban_mobile"
create="0">
<field name="assignment_id"/>
<field name="student_id"/>
<field name="score"/>
+2
View File
@@ -0,0 +1,2 @@
# recents_limit —— 纯前端补丁模块,无 Python 模型
# 仅通过 JS patch 把导航栏「最近」分组上限从 50 改为 20。
+26
View File
@@ -0,0 +1,26 @@
{
'name': 'Recents Limit (20)',
'version': '18.0.1.0.0',
'summary': '把导航栏「最近」分组上限从 50 改为 20(继承 udoo_om_ux,不改动线上源码)',
'description': """
通过前端 patch 继承 udoo_om_ux 的 OmuxNavBar,把「最近访问」的分组上限
从默认的 50 截到 20。
实现方式(不改动 udoo_om_ux 与 yuthon_base 的任何源码):
1. setup 时把历史 localStorage 中可能 >20 的 recents 截断到 20;
2. 监听 UDOO:FRC 事件,在每次新增「最近」后把上限压回 20。
由于整个会话期间 this.uo.recents 恒 <= 20,beforeunload 持久化到
localStorage 时自然也 <= 20,无需再 patch beforeunload 逻辑。
""",
'category': 'Tools',
'depends': ['udoo_om_ux', 'web'],
'assets': {
'web.assets_backend': [
'recents_limit/static/src/js/recents_limit.js',
],
},
'installable': True,
'application': False,
'license': 'LGPL-3',
'author': 'Yuthon',
}
@@ -0,0 +1,29 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { OmuxNavBar } from "@udoo_om_ux/webclient/navbar/navbar";
import { useBus } from "@web/core/utils/hooks";
// 「最近访问」分组的目标上限(线上 udoo_om_ux 默认为 50)
const MAX_RECENTS = 20;
patch(OmuxNavBar.prototype, {
setup() {
super.setup();
// 1) 处理从历史 localStorage 加载进来的 >20 条「最近」
if (this.uo?.recents?.length > MAX_RECENTS) {
this.uo.recents.length = MAX_RECENTS;
}
// 2) 实时:每次新增「最近」访问后,把上限压回 20。
// 原 OmuxNavBar 在 UDOO:FRC 事件里会把上限维持在 50,
// 这里在其之后(先注册者先触发)再截到 20,
// 于是整个会话期间 this.uo.recents 恒 <= 20,
// 连带 beforeunload 持久化到 localStorage 也自然 <= 20。
useBus(this.env.bus, "UDOO:FRC", () => {
if (this.uo?.recents?.length > MAX_RECENTS) {
this.uo.recents.length = MAX_RECENTS;
}
});
},
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB