From 30587b58b13bfe3a6243ea68f166f621c2a3c95b Mon Sep 17 00:00:00 2001 From: hrrr Date: Mon, 15 Jun 2026 23:57:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Eedu=5Fbase=E3=80=81learning?= =?UTF-8?q?=5Fcenter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- edu_base/__init__.py | 0 edu_base/__manifest__.py | 15 + edu_base/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 147 bytes edu_base/security/groups.xml | 26 ++ learning_center/__init__.py | 1 + learning_center/__manifest__.py | 34 ++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 188 bytes learning_center/models/__init__.py | 10 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 578 bytes .../course_chapter.cpython-312.pyc | Bin 0 -> 2954 bytes .../course_chapter_resource.cpython-312.pyc | Bin 0 -> 5240 bytes .../course_homework.cpython-312.pyc | Bin 0 -> 10263 bytes .../course_homework_submit.cpython-312.pyc | Bin 0 -> 5826 bytes .../course_score_item.cpython-312.pyc | Bin 0 -> 3797 bytes .../course_student_score.cpython-312.pyc | Bin 0 -> 7795 bytes .../course_syllabus.cpython-312.pyc | Bin 0 -> 4843 bytes .../course_teaching_class.cpython-312.pyc | Bin 0 -> 16823 bytes .../learning_course.cpython-312.pyc | Bin 0 -> 6893 bytes .../__pycache__/teach_class.cpython-312.pyc | Bin 0 -> 1263 bytes learning_center/models/course_chapter.py | 32 ++ .../models/course_chapter_resource.py | 80 +++++ learning_center/models/course_homework.py | 179 +++++++++++ .../models/course_homework_submit.py | 102 ++++++ learning_center/models/course_score_item.py | 54 ++++ .../models/course_student_score.py | 112 +++++++ learning_center/models/course_syllabus.py | 80 +++++ .../models/course_teaching_class.py | 291 ++++++++++++++++++ learning_center/models/learning_course.py | 113 +++++++ learning_center/models/teach_class.py | 13 + learning_center/security/ir.model.access.csv | 24 ++ learning_center/security/record_rules.xml | 41 +++ learning_center/views/view_course_chapter.xml | 135 ++++++++ .../views/view_course_chapter_resource.xml | 107 +++++++ .../views/view_course_homework.xml | 123 ++++++++ .../views/view_course_homework_student.xml | 103 +++++++ .../views/view_course_score_item.xml | 119 +++++++ .../views/view_course_student_score.xml | 204 ++++++++++++ .../views/view_course_syllabus.xml | 134 ++++++++ .../views/view_course_teaching_class.xml | 215 +++++++++++++ .../views/view_homework_submit.xml | 140 +++++++++ .../views/view_learning_course.xml | 170 ++++++++++ learning_center/views/view_teach_class.xml | 51 +++ 42 files changed, 2708 insertions(+) create mode 100644 edu_base/__init__.py create mode 100644 edu_base/__manifest__.py create mode 100644 edu_base/__pycache__/__init__.cpython-312.pyc create mode 100644 edu_base/security/groups.xml create mode 100644 learning_center/__init__.py create mode 100644 learning_center/__manifest__.py create mode 100644 learning_center/__pycache__/__init__.cpython-312.pyc create mode 100644 learning_center/models/__init__.py create mode 100644 learning_center/models/__pycache__/__init__.cpython-312.pyc create mode 100644 learning_center/models/__pycache__/course_chapter.cpython-312.pyc create mode 100644 learning_center/models/__pycache__/course_chapter_resource.cpython-312.pyc create mode 100644 learning_center/models/__pycache__/course_homework.cpython-312.pyc create mode 100644 learning_center/models/__pycache__/course_homework_submit.cpython-312.pyc create mode 100644 learning_center/models/__pycache__/course_score_item.cpython-312.pyc create mode 100644 learning_center/models/__pycache__/course_student_score.cpython-312.pyc create mode 100644 learning_center/models/__pycache__/course_syllabus.cpython-312.pyc create mode 100644 learning_center/models/__pycache__/course_teaching_class.cpython-312.pyc create mode 100644 learning_center/models/__pycache__/learning_course.cpython-312.pyc create mode 100644 learning_center/models/__pycache__/teach_class.cpython-312.pyc create mode 100644 learning_center/models/course_chapter.py create mode 100644 learning_center/models/course_chapter_resource.py create mode 100644 learning_center/models/course_homework.py create mode 100644 learning_center/models/course_homework_submit.py create mode 100644 learning_center/models/course_score_item.py create mode 100644 learning_center/models/course_student_score.py create mode 100644 learning_center/models/course_syllabus.py create mode 100644 learning_center/models/course_teaching_class.py create mode 100644 learning_center/models/learning_course.py create mode 100644 learning_center/models/teach_class.py create mode 100644 learning_center/security/ir.model.access.csv create mode 100644 learning_center/security/record_rules.xml create mode 100644 learning_center/views/view_course_chapter.xml create mode 100644 learning_center/views/view_course_chapter_resource.xml create mode 100644 learning_center/views/view_course_homework.xml create mode 100644 learning_center/views/view_course_homework_student.xml create mode 100644 learning_center/views/view_course_score_item.xml create mode 100644 learning_center/views/view_course_student_score.xml create mode 100644 learning_center/views/view_course_syllabus.xml create mode 100644 learning_center/views/view_course_teaching_class.xml create mode 100644 learning_center/views/view_homework_submit.xml create mode 100644 learning_center/views/view_learning_course.xml create mode 100644 learning_center/views/view_teach_class.xml diff --git a/edu_base/__init__.py b/edu_base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/edu_base/__manifest__.py b/edu_base/__manifest__.py new file mode 100644 index 0000000..e12edf1 --- /dev/null +++ b/edu_base/__manifest__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +{ + 'name': '教育基础权限', + 'version': '18.0.1.0.0', + 'summary': '通用权限组:学生、教师、教务管理员', + 'description': '统一管理全项目通用角色权限组,供其他业务模块依赖使用', + 'author': '', + 'depends': ['base'], # 只依赖系统基础模块 + 'data': [ + 'security/groups.xml', + ], + 'installable': True, + 'auto_install': False, # 不自动安装,手动安装一次即可 + 'application': False, +} \ No newline at end of file diff --git a/edu_base/__pycache__/__init__.cpython-312.pyc b/edu_base/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..665b658f8d2eb879cdefa9689688242c9fa285c9 GIT binary patch literal 147 zcmX@j%ge<81RgDVSs?l`h(HIQS%4zb87dhx8U0o=6fpsLpFwJV8Kqdo~9rSVCL#i=pz@tJv + + + + + 学生 + + + + + + + + 教师 + + + + + + + 教务管理员 + + + + + \ No newline at end of file diff --git a/learning_center/__init__.py b/learning_center/__init__.py new file mode 100644 index 0000000..9a7e03e --- /dev/null +++ b/learning_center/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/learning_center/__manifest__.py b/learning_center/__manifest__.py new file mode 100644 index 0000000..22aa1ac --- /dev/null +++ b/learning_center/__manifest__.py @@ -0,0 +1,34 @@ +{ + 'name': 'Learning Course', + 'version': '18.0.1.0.0', + 'category': 'Education', + 'summary': '课程管理 - 课程基础信息', + 'description': """ + 学习中心 + """, + 'author': 'hrrr', + 'depends': [ + 'base', + 'mail', + 'student_organization', + ], + 'data': [ + 'security/ir.model.access.csv', + 'security/record_rules.xml', + 'views/view_learning_course.xml', + 'views/view_course_chapter.xml', + 'views/view_course_chapter_resource.xml', + 'views/view_course_homework.xml', + 'views/view_homework_submit.xml', + 'views/view_course_score_item.xml', + 'views/view_course_syllabus.xml', + 'views/view_course_teaching_class.xml', + 'views/view_course_student_score.xml', + 'views/view_course_homework_student.xml', + 'views/view_teach_class.xml', + + + ], + 'installable': True, + 'application': True, +} \ No newline at end of file diff --git a/learning_center/__pycache__/__init__.cpython-312.pyc b/learning_center/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8865f2490397fba4da9ddd944a2c75905034f013 GIT binary patch literal 188 zcmX@j%ge<81ReI8St3CCF^B^LOi;#W0U%>KLkdF*V-7^CU4!Y2e_V}Mz>cC;Dd_wYOG-Ki0}e2#4Lr3amI)n z$PL~wY$03RGHfH;+&0`qZt|w#7IKTX47ZWnylvP)cDQ5MMRvJs*hBWX$6)3UJD+$q z7_xaUm7&U${6<7YrL;t~@k1h-gws+gG&}!|P*US?M8myROV83Y99Q~}niQFQDAXOQ z?bXy;30;k|q{L2ty+10H6iF#Fl$&c=R}YvoHBMiX=9G}QD;-xv{5c|G*! zpe+16Wm7aMS`=*xhr*?x|D=414#q1iFkyKap9Mu+6ep*Fmg-)rAe)6)=Q@yT8vI@t z9*mkrEK?l_k>p7!#ArHOI_D^>R9>h)K9KHN4=}zg##jyeHSE=JYykPFhJzXoYZ$!4 IuIl03H`+F$AOHXW literal 0 HcmV?d00001 diff --git a/learning_center/models/__pycache__/course_chapter.cpython-312.pyc b/learning_center/models/__pycache__/course_chapter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c803a39a3c806a96c874494c1da07894c2aef45 GIT binary patch literal 2954 zcmbVOT})I*6rQ{Pz_L&f5anl=AL~}SMq{W^f>tcmiuj;WyFqU+I|D4-y}QocHGx>W zX-UQY1opvVUm9#1#Xuu%($x6S_OZ4PE@h3IsY#nOi}=!5XjoO5P>DlE)L(5}}u^@nW;{mv&wvt|d2`(QARFv5b2LV{linf#`Z*>4t*3B8K2 zX&7NMG50$R#pQm=tdITHL5tV=kWaj3W|ksBX6p$O8LP||3S%Oxfn_Cxz!R-4{wVM- z6XX{#@|&>WH)E6Eg3W#_w)6^q8@BrGCPW1|1i@xqJ*06QCD1D4ez{BM)m#pbw zDPA{Sw&Y>ynz-^6xOHpdDpugiG+fgv{;kKAcmtf;xW$S!ksAS7m4jT5tA~-l64!v- zfNMc+9CUjI>-em=l~>xbf|S$@d>jO)R|kHnS*Fhzu2k zq@e7J_E93?p$c6`4B{rYO3uWH5+F_PAl8(1wanOu53bD|^O~4Vjnbgf%N!JtK}v9p z$5!UnXlC@AhdlJXCYlEwvt!aD#pGy?mHE-ph3{s(f(95Wl8HnWu;tUguL0*T#xq}j zqI=Bj0Z9!=n0U>a2Ic0NBdkDplPJ;gmv3aou4YCPnVauu3kzQl&)*uGzclmk!YHc{ z1L06479}DjDi9MOun<$COFm||bsN>uRXZ+)mE6yUl|Ex(*Sn$WaR;}_y>q_ zbPnxDry>ZwDMXi0=Y;-j8gd1r8Od}`5SMoUOQ+v%XfO9z7)FLIqX(PW^h4;GY(h~p zY#BY+G)f}+fLhQX)UwVMRPbYP$`;)qI2HZSKi#k}uo&F_@|`2Mk0iyOpF4UM z`2km_f!D{iX*jEs09k5|;2sc>C@ljGS^+{oJ>}n|ycj>T*Ie1~+H+^??XAgE1GBFU z(2cO}wK20y6y{uFKL{_9YiXusA9M5sWuTT|R-j#%o|G&PhXY1_GK)&&9%iE?5T;n= zao~3A&KGg#8~+#O{BcrGfrz7D$~TO(-*0$+(spn2?%B<|=Nei@+EY%?eNV&0@q3Ls zXB&6kYkYYYe*1dn8hhtFeSEv-er?@E^S%1!*?N$BX5qJYV6I-Cs|}5`-*r~~X-5?s zwUhtrzO)SHuP8k9!Z{?uNN*73rp{v!k$Tsvmr-HrENyvJ;)& zD&`PH{`80<%NIpRs+i2jPEkA)ljQ7{Es*N;;EZ;9CtLelmU${fZ^#w-V-A(<8AnsgkNl)>#`g|Iw`NO(S>cr*LH za2S$bpPq!F(NE3dAU3my(C z&+K88c2>n-Ntb%!9qHoo_`!5>^~hWC1L?v|@%G;e%14?d((?!)I zXGhOpJU?-AhNg;k0kEWcY}5Fb#FmNEsgfP>L+Rq$G23`iqA2O!cGH(UdLr39kSdnJ zaH(gkdAv2zI$87mx>RX9e6Os2?D+WU#OcY_WLsCN>{PrnUE&67zJzbmmE8M!s>II? z)__4*!Zo2JTTi5lPjXXrU}{fdPqMz{rkp$~rAoWw9gl2iUA?isV@6IMdLvaT#XBCG zELKH$>=dk?MVnx))A`^mbV7l!@Zw83AKL9v{sb`VVg9F8V~XzOTfCZR6hfWVqotL$ l!5XjOD&Kr$76jo})G&)0{<2gHbz|-09f^*=5ezlue*hqS0^$Gw literal 0 HcmV?d00001 diff --git a/learning_center/models/__pycache__/course_chapter_resource.cpython-312.pyc b/learning_center/models/__pycache__/course_chapter_resource.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a09887021324d423735b9955f40cadf5e82c085 GIT binary patch literal 5240 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 ZWdETWVpPLV%4cM<9ek|(m%@Fb{y#|C5eNVP literal 0 HcmV?d00001 diff --git a/learning_center/models/__pycache__/course_score_item.cpython-312.pyc b/learning_center/models/__pycache__/course_score_item.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26bd81a4f67d2b7a8090001f91a287f0fb437b9a GIT binary patch literal 3797 zcma)8Yiv{39lzJtuOv=vC&31i5C?~V3z+8+D`A=-4Ua-5m2T@?7wgLB#Bpk0r}th5 zlBVDa+8RNLJ<*!2X)){6(j_gKDoug3TR$T0i?eycy_>WTixcvVHSHHZ?SIa_cAVCc zcBP!0$M64s`LFf$wG4c(eX)J;9PIu>4yDhaZI+L~Cc_{Gu@NT9`q-$>r;Bnvj%9Sr zF$U=_Gl;|7pt(f2@_&+3_ddgj-eXuI2M?!k{7_gi_J?rXPYr$eldLpq-xaXtgm`;5pCVtpnZgUy4DmE`Ik8IftqJgHMpzS=c$HB+^d ztcI$isP$F2)gcQqq57$YNxg=xUz4L1*+90vY<SZV8ThSe*+Ov#tKilnH$=T8w~gYeS>1`YOm&rUo7U8C>ndFP znz(JNaIQ6Ro>jOGitDLj$5UuK+5wi7>dcLHUS@oq$P4S1kzJnMiuGtLE=u@JFec&? zGLDYggY?2HksEIquHqKpypT*S&VI6V``+SvpDB8QkK)mdDl!m8J6z{*I5Z%;5SD^F zTwyd?7v;kduRI`P9#MLgvY2{%@#E>|JvzlG$zoUtDP|GB5)X^G616Zs{^&2$9#*UY z!K2m)=0yRpUR7S4l>eiMB;p7!V?^nRHav<6k&#N$AatM#()rmB7qhRA+E#c6LH)2y^(ub)Q8D@EXw%9g zKF>!3^2m_JB5`RUJG1caz2X<+qgEB=9f(EoxtRF!cf_*!FDK?_eoL7xT>o|P{`e9x z%A->iG$Gv2$0M@G1U@vE2t_M}D0(7H^-to`qr2|_rWT(c!eTgz1x>-vK3Mw0HS)mA zG8Pao1aXO?c=h-2V2Xy3j72T%6SwAPK7CZ;X6O$Kd_+x%iCYU3lPfPVc>s%)Q}K;I z&CkB8f@sz({&BYW!PPj4=ED1zmfm`M+ZK;%;OuSiRtWCl`_)fXlACvnsgH`OyNfsO z(2L?5pFev2hUONMis0kL8@J}~-(PrVT4@LbW6`0wj02L)%Tn1-+AS4Yy!UzW{kK4p zs=`EKY4T>pY5vRWaC*|BMMIL~2)v~g0Kzh4vQkgWAwbFj^1Z^gg`3wGXWtBJ1A-bt zijDj&AAlvpj4?gTOU*Du*)evI0#+{;W{eG#udX@t%n0LQ`#h{t6P5yeP!11c2#a3A zk$%M}VyH_bk=EpLJvo7%_Qy~xw&$QUZroG+m5+Vhdb|t*|KUVZ}gWE0QenSQHKm!_aV{ zjRpr4T^PZSDt>4PBZZ4#0a9B{cqpW}AS;{{k193%a40}|C|s03uhhU&K8FQ3hXwWQ z(TnXwvWrND?nJ5uezj3rNg@$(!6$YQ3<=-prtfFe>H^*g7-<}qQZ6|*W(Lv&x%Tck z$Ki*Lo;gQP!S2esv%?>c+#bo<_B`fvy==oWqjOr5#~vFQM_cA>`fT>t-`DL(_WsM= zl|A=icd|Ecaedu=WJ1awc<^&UF0nOtRMw*(*aY^p49+!P1_1rqI-pI90GY5+@4vo$(#?g~qm2S7v*9d-iO;aew03f~`Ge%vjUboNLd_wg=DW{5Wsx2RO7&(=rBCHNhrT+V`{j%IW`E*z!MQax{Lr~|&bc+W`vpH3Vv*gH~= zhxU#+d&f77*=>AmW2|ji`CGfWacAP><3^@oWAYc{{ww}$3z%2m4VK&6QjSb(x)pfD z^Y+7ulLgx*Ft8!rkQHW7-gXpBYjUR!JZy5$HMw)0KbjHpO{Wv5z*p-Xp?_;}D{h3HQ@*M+PO8h^7+8T%e literal 0 HcmV?d00001 diff --git a/learning_center/models/__pycache__/course_student_score.cpython-312.pyc b/learning_center/models/__pycache__/course_student_score.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dba182aa80fd645dbf1a125b1c10f08156738371 GIT binary patch literal 7795 zcmds6Z*WuBm4Etc;Xl~2jRiKg<^L8oa-gllp8y48LyQdt5=w+XRdt_jfh;*!GGL9n zQJOTEhQxNWU7O9Oh^ATW0Rq|GHg2FOwpM9XVV=PHvVey?k!wk?Zs>VQ-K9h`=t zEQ?_YhgmeD#yQ{?^*wIhL;Rw?-$!_l-=Y%pk2yVV;taTb-gZiT)RkC~+L}k+uwM|PjHXYI0^hDRILa}(m>B8jUQk&>W7BSpd72iNMaORu_b*+3z zHp#iJmPh5j%UzclYH9y&U<$Z&jpK=2vE} z2LAN-;AOH!*+1EubPsBVwpq4|g>Ak;>gSIJ1_|#C9Ci69|Ge16`mBDb)9T0GZ_tg* z-~7kLzrC_BGPZbUTFfPG|A5CiZ9~cbqj^w#so7RYWe}KBZy<(QkAnN=9kV4Fnm%%kf11!W= zcd3$$1XY6m?dcI96WlPjmpch6|Cwr7bvlVnX?_g86u!tNqV(eL$J{5HVU;8P^|c2ZQ5e@^YkoF z?fpYec=!74JRPumcqjG3Rvj)l0Q|_jsa@WuqDpoK)`20q8YT@kw!Ls_*0icbx%^E) zLfl_VN=G_kbxqeCR~A82+u71-5=-g zjs}YoEc|(Azaua-z$Y(;mSWLTumFy&NX^))0`fdpv2eGy$Hz7(8MsBA;EYQ%`em?C zIStqYGrhrn_IYvi=Hge^f<;N6T^^_34;O%ae>C&aqnW=?`QGn5?W6ELrIXDG;<8Oj zqRe0W2=2+!`~^Pl~xaS!WilelJXF5bM@*v7i{vab2j zUoM32HtuCz9c<-CGatyk9Tv$PZeB!l$X68ampo{&AklG-clY)MQrs4%c+7463Y?kj zw*C#>7E%37o!)hUW7`fKnqf^k_vKezDu6F?gn-%O4}2*fR;M48enb$YBkCZylVBdK zsJ^sA%LTHO35V50qr@0+nDG@sYJu-$009lds;HMtLDW&-pqGd_Nd_zC^iuG$4iA5p_kat-ESIhWBI+2I zsF$v~9~Ewfhn6D81|Y$Pwr`0LNo%0M{;z;20ixJ6(jF@{&z5e8mTnl+$BNcXyzbTWC-5n|4I@1;@-xVp|HQv6gAC8L@Zgm5)5?*K zXl}(^{+ikRsz`oS_{f9&hFD$WnEuZN)iI-K)>soU)`aN;qvik7J15Wdqzd8{Ed%in zT53gugCzqFhnVefKo}kL@X*e6INls|dN2&Yn~t$SC*l1r>K;I>BCc{k#Fjqj9T2&B z3Uj%=eLQssC@NRfw)HtFi}DojsF+3g0p3geEF6o5Zl`x>v(L+mtKz{Zo~dB46LlRP zpEDrl$)8;;mWiv5LacN#5tKj|4`D2?5VI90?k0)0*OzeaN}NgbD3OI_C!*nym*3nE z;?XTAxA6sMDi*EiVwsg`MW^Dc#j+J2l3g)n=Dq{gVKz9;v6GRE68!*0g6i`FuHxS1 zV!73!_E=F>s4G@b5qdS2R~zbx6_k&>71|#wsu*jX*gn2J>=kytE}V2mi+&pFn$s18 zj$SzN_KC6baP~u8(-XB?cTn|I!|9usS##OI8O>w*iTv?=!Mtgv@&0RqosSmugkGI1 zEFY_%Xc}({pAvR-3&-uz!jqu`v10RB$3)k7*OWon)+PM-M6}o*>W-Nj#?H=~8X~5K zslO5S*@fOSQIki|7RQuHg!1=~S6Dq(KU-KGDXji)PG75E&gJq= zg1L2uJj>G;?Nx;iEEjNkqjYqFv0A8YeW-i+iQ1^M;Mr|qEy-5ULR|1a{wOi#>DOQ~ z{|~ALh;j8d3gZ}8svD%k?M0**5b>I%YJ`zt!rHiQ;tc|ogBxp=2y83L$hVG^!kmU1 zW%2c;8bi7{*OT%Uyi$z;BgTpc>IRi7c%|Awx(bf@M%8s0uGuoz4XQ~Esg>uHa|D}I zJ4mOiI)W7eX=e(zR6R(yvqoY`!P;dRu`^T;6s?lxBR4N_$-5KDY!69Y|W(G^vHNWPVzkAi?rfeXjsSv>thkQW7A60xKS0 z5;9x5`p3nOKSwZ(1rS$2!cbpN02m%z1EU|``b=V(|NJ*gmwz$OzQ}l0)b{b70R~eR zK6z*9!Uv6YmSAy;8m&^c2|7mbErDXRI$8sSt+jCHDgy7dsl$2(Ju-?)l6CQmJM+JJ zesr`yqx#vL1 zIWd2w8eX_JE?^68`tEzPVxphIFhAZOKm7VFg6Tf@`CGd;6A z4@KZ#%OT;&>yeh%g&!Y(Xn6xMK>eJhnf*RlrDX6ahk|(kJCxN4FdX`pLphLM<#bni zrKjgUvlmoXpEyQV7X(38K2R}4K%T}3;EU!Hof6)Do}uCh@Vhs?TmX$>R``A{Ns6TT z*wO0`4q{d$UH!&EEJ9kK2J!fvKRy1#FCPEv1?dJiK37>NipboC#4K?=n)&MSzg$`P z^=&A@F>7IVqX$;H(n0_yx%zn58L5C0tos&INwA@n`zerwd5jMKayYD?HE()g-gLX? zOV1acd&g$mk44&#J!n1lmy)`vtlRo8bHB)ibbH5P;mGmXBd*91mvEQ}gcs_29+vb9 z1--HSHKW7t!#vk=ujWp@`Oy3ljD$@0VaZORU}wUR4q7Vd4{10EH5kpQ5U$4GCgcDVW)@g#^X-{~qigk&*?GQbQamS-%4*#T29@ z7@Gy_o`KYu;ZRraJU5Txu^-yL2<0YGTC$G%;ht} z=EK4Xhj7}zjBc${{fy%(52yug`CM@gK&M0FhlJXf@2wWz0GvRh#r{zDvYyLZH_|cM zb+IdK5L%!NurJgOovTX$%GfX3!(~Eim+;zg!POhh>w|q5%K;~?A739nF1&o0P2`Ch z`w>7@jg`zAt0Km#sS08D2^b*Ivr*%@Q0JV2i^7Izo+Z>Vm%u}S`h>1NVIUB*Yy{k5 zYys?-jLwA4UVu8W4OaMA_)@47Bs0|jYVwSGrmVsqyFmJ)CGOCnxCX~elg$rx4NufM z-A05fjmy|tUd`!?0nwa#`_$OJN&7?H24vWU3;;`^C7mdq<3Y{b8( literal 0 HcmV?d00001 diff --git a/learning_center/models/__pycache__/course_syllabus.cpython-312.pyc b/learning_center/models/__pycache__/course_syllabus.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..25acc02a1b5fa322c418fe0d5e290fa4c030a7ca GIT binary patch literal 4843 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 E06yM1s{jB1 literal 0 HcmV?d00001 diff --git a/learning_center/models/__pycache__/learning_course.cpython-312.pyc b/learning_center/models/__pycache__/learning_course.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ddb1005dde3ab97e07b068cb7ae2ef749db55994 GIT binary patch literal 6893 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 B@LK=? literal 0 HcmV?d00001 diff --git a/learning_center/models/__pycache__/teach_class.cpython-312.pyc b/learning_center/models/__pycache__/teach_class.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9652096e02b63942ab93f9066c6c700591d8862f GIT binary patch literal 1263 zcmZ`(OH30%7@qC+{Q{}-mMAewH7U^;V!%KY5fs6Lilj8zY&rv!Y`1l{CA0^rkwY7! zE%8xEOuR`9ka#c=IdLTMQj#V%bMasb@n(WvI5{)hN=2E&?Dx~L5CS#7V{ zrylD7_(p~DGL*pXRRXdA0?0`q$a%S-&Z`UZUY-Lw&<~()4nQ99Q|7Ya{&$(zu-6dN zyA9u|;N~$eh6C6*9zYUQu`w8eNK*a#MgZtb!fqQ!BO~DDAn@uS=jC+(>8DuEl?H%h z0r?gEGO2+suklcx0U9fKCdRAQtSWbCg60*=AAgoZc~)qv;8_{3w$hV^p&dFZc=kj6 zIbl@=&%t>AtG5~+so*(dHEtKS-bJE+vR4vSHExSg?C~?r$dlu8-{qbT(9Ve^nE z$1ad?xYRn~@77`Keq5~YofH*pA}6#V*vtTlhAnrHg#3{}NY=EZK5U^{RtctU7Rm{~ z1G^+J*z1Lzz7PzB&R_DWNSQ{8FBlVvEUP}Ggnbec6`4(t%!ex3>+!Mdw5fW+F~vkY zv5mfA^%>dbK*8RS=qoh37f!!vUu@5vy|USqZ@ib&Hx#N`lGEwg)a=5;_oMl$?!?W4 zy)Jn@J(wEIow&F*khk|G`U{Tw deadline_val) + record.submitted_count = len(on_time_submit) + record.late_count = len(late_submit) + else: + record.submitted_count = total_submit + record.late_count = 0 + + # 未提交 = 班级总人数 - 所有提交人数 + record.unsubmitted_count = record.total_students - total_submit + + def action_publish(self): + self.state = 'published' + self.publish_time = fields.Datetime.now() + + def action_draft(self): + self.state = 'draft' + + def action_close(self): + self.state = 'closed' + self.is_submit_open = False + + @api.constrains('deadline', 'late_deadline') + def _check_deadline(self): + for record in self: + if record.late_deadline and record.late_deadline < record.deadline: + raise ValidationError('补交截止时间必须晚于提交截止时间') + + def _get_current_student(self): + """获取当前登录的学生""" + student = self.env['student.info'].search([('user_id', '=', self.env.uid)], limit=1) + return student + + # 当前学生的提交记录 + my_submit_id = fields.Many2one('course.homework.submit', string='我的提交', + compute='_compute_my_submit') + my_submit_state = fields.Char(string='提交状态', compute='_compute_my_submit') + my_submit_time = fields.Datetime(string='提交时间', compute='_compute_my_submit') + my_score = fields.Float(string='我的得分', compute='_compute_my_submit') + my_grade_level = fields.Char(string='我的等级', compute='_compute_my_submit') + my_comment = fields.Html(string='我的评语', compute='_compute_my_submit') + my_submit_file = fields.Binary(string='我的提交文件', compute='_compute_my_submit') + my_submit_filename = fields.Char(string='文件名', compute='_compute_my_submit') + my_submit_content = fields.Html(string='提交内容', compute='_compute_my_submit') + + # 提交状态(用于前端显示) + submit_state = fields.Selection([ + ('not_submitted', '未提交'), + ('submitted', '已提交'), + ('graded', '已批改'), + ('late', '逾期提交'), + ], string='提交状态', compute='_compute_my_submit') + + # 是否可以提交 + can_submit = fields.Boolean(string='可以提交', compute='_compute_can_submit') + + @api.depends('submit_ids') + def _compute_my_submit(self): + for record in self: + student = self._get_current_student() + if student: + submit = record.submit_ids.filtered(lambda s: s.student_id == student) + if submit: + record.my_submit_id = submit.id + record.my_submit_state = submit.state + record.my_submit_time = submit.submit_time + record.my_score = submit.score + record.my_grade_level = submit.grade_level + record.my_comment = submit.comment + record.my_submit_file = submit.submit_file + record.my_submit_filename = submit.submit_filename + record.my_submit_content = submit.submit_content + + # 计算提交状态 + if submit.state == 'graded': + record.submit_state = 'graded' + elif submit.is_late: + record.submit_state = 'late' + else: + record.submit_state = 'submitted' + else: + record.submit_state = 'not_submitted' + else: + record.submit_state = 'not_submitted' + + def _compute_can_submit(self): + for record in self: + # 作业已发布、提交通道开启、未截止、且未提交 + can = (record.state == 'published' and + record.is_submit_open and + record.deadline and + record.deadline > fields.Datetime.now() and + record.my_submit_state != 'submitted' and + record.my_submit_state != 'graded') + record.can_submit = can + + def action_submit_homework(self): + """学生提交作业""" + self.ensure_one() + student = self._get_current_student() + if not student: + raise ValidationError('请先关联学生信息') + + # 检查提交权限 + if not self.can_submit: + raise ValidationError('当前无法提交作业') + + # 打开提交表单 + return { + 'type': 'ir.actions.act_window', + 'name': '提交作业', + 'res_model': 'course.homework.submit', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_homework_id': self.id, + 'default_student_id': student.id, + }, + } \ No newline at end of file diff --git a/learning_center/models/course_homework_submit.py b/learning_center/models/course_homework_submit.py new file mode 100644 index 0000000..681e245 --- /dev/null +++ b/learning_center/models/course_homework_submit.py @@ -0,0 +1,102 @@ +from odoo import api, fields, models + +class CourseHomeworkSubmit(models.Model): + _name = 'course.homework.submit' + _description = '作业提交记录' + _rec_name = 'display_name' + _inherit = ['mail.thread'] + _order = 'homework_id, student_id' + + display_name = fields.Char(string='显示名称', compute='_compute_display_name', store=True) + student_id = fields.Many2one('student.info', string='学生', required=True) + homework_id = fields.Many2one('course.homework', string='作业', required=True, ondelete='cascade') + @api.depends('homework_id.name', 'student_id.stu_name') + def _compute_display_name(self): + for record in self: + record.display_name = f"{record.student_id.stu_name} - {record.homework_id.name}" + + course_name = fields.Char(related="homework_id.course_id.name", string="课程名称", readonly=True) + course_code = fields.Char(related="homework_id.course_id.code", string="课程代码", readonly=True) + course_credit = fields.Float(related="homework_id.course_id.credit", string="学分", readonly=True) + + homework_name = fields.Char(related="homework_id.name", string="作业名称", readonly=True) + homework_total_score = fields.Float(related="homework_id.total_score", string="作业总分", readonly=True) + homework_deadline = fields.Datetime(related="homework_id.deadline", string="截止时间", readonly=True) + homework_late_deadline = fields.Datetime(related="homework_id.late_deadline", string="补交截止时间", readonly=True) + + student_name = fields.Char(related="student_id.stu_name", string="姓名", readonly=True) + student_no = fields.Char(related="student_id.stu_num", string="学号", readonly=True) + student_class_id = fields.Many2one(related="student_id.class_id", string="班级", readonly=True) + student_major_id = fields.Many2one(related="student_id.major_id", string="专业", readonly=True) + student_phone = fields.Char(related="student_id.stu_phone", string="手机号", readonly=True) + submit_file = fields.Binary(string='提交文件', attachment=True) + submit_filename = fields.Char(string='文件名') + submit_content = fields.Html(string='提交内容') + submit_time = fields.Datetime(string='提交时间') + score = fields.Float(string='得分') + comment = fields.Html(string='批改评语') + grader_id = fields.Many2one('res.users', string='批改人') + grade_time = fields.Datetime(string='批改时间') + state = fields.Selection([ + ('submitted', '已提交'), + ('graded', '已批改'), + ], string='状态', default='submitted') + is_late = fields.Boolean(string='是否逾期', compute='_compute_is_late') + + + def _compute_is_late(self): + for record in self: + deadline = record.homework_id.deadline + if deadline and record.submit_time: + record.is_late = record.submit_time > deadline + else: + record.is_late = False + + def action_grade(self): + self.state = 'graded' + self.grade_time = fields.Datetime.now() + + def action_regrade(self): + self.state = 'submitted' + self.grade_time = False + + # 在 CourseHomeworkSubmit 类中添加 + grade_level = fields.Selection([ + ('A', '优秀(A)'), + ('B', '良好(B)'), + ('C', '中等(C)'), + ('D', '及格(D)'), + ('F', '不及格(F)'), + ], string='等级', compute='_compute_grade_level', store=True) + + @api.depends('score', 'homework_id.total_score') + def _compute_grade_level(self): + for record in self: + if record.score and record.homework_id.total_score: + percent = record.score / record.homework_id.total_score * 100 + if percent >= 90: + record.grade_level = 'A' + elif percent >= 80: + record.grade_level = 'B' + elif percent >= 70: + record.grade_level = 'C' + elif percent >= 60: + record.grade_level = 'D' + else: + record.grade_level = 'F' + else: + record.grade_level = False + +# class CourseHomeworkSubmitBatchGrade(models.TransientModel): +# _name = 'course.homework.submit.batch.grade' +# _description = '作业批量批改向导' +# +# homework_id = fields.Many2one('course.homework', string='作业') +# total_count = fields.Integer(string='待批改总数') +# score = fields.Float(string='统一分数') +# comment = fields.Text(string='统一评语') +# +# # 批量批改执行方法 +# def action_batch_grade(self): +# # 自行补充批量更新分数、评语、状态的业务逻辑 +# return {'type': 'ir.actions.act_window_close'} \ No newline at end of file diff --git a/learning_center/models/course_score_item.py b/learning_center/models/course_score_item.py new file mode 100644 index 0000000..d04617c --- /dev/null +++ b/learning_center/models/course_score_item.py @@ -0,0 +1,54 @@ +from odoo import api, fields, models + +class CourseScoreItem(models.Model): + _name = 'course.score.item' + _description = '成绩项目' + _rec_name = 'name' + _order = 'course_id, weight desc, id' + _inherit = ['mail.thread'] + + + name = fields.Char(string='项目名称', required=True) + description = fields.Char(string='项目描述') + course_id = fields.Many2one('learning.course', string='课程', required=True, ondelete='cascade') + course_name = fields.Char(related="course_id.name", string="课程名称", readonly=True) + course_code = fields.Char(related="course_id.code", string="课程代码", readonly=True) + course_credit = fields.Float(related="course_id.credit", string="学分", readonly=True) + course_exam_type = fields.Selection(related="course_id.exam_type", string="考核方式", readonly=True) + homework_id = fields.Many2one('course.homework', string='作业', required=True, ondelete='cascade') + sequence = fields.Integer(string="排序", default=10) + score_type = fields.Selection([ + ('homework', '作业'), + ('exam', '考试'), + ('experiment', '实验'), + ('attendance', '考勤'), + ('midterm', '期中考试'), + ('final', '期末考试'), + ('other', '其他'), + ], string='成绩类型', required=True) + weight = fields.Float(string='权重(%)', required=True) + full_score = fields.Float(string='满分', default=100.0) + is_active = fields.Boolean(string='是否启用', default=True) + homework_ids = fields.Many2many('course.homework', string='关联作业') + + # 统计字段 + student_count = fields.Integer(string='学生人数', compute='_compute_stats') + avg_score = fields.Float(string='平均分', compute='_compute_stats') + max_score = fields.Float(string='最高分', compute='_compute_stats') + min_score = fields.Float(string='最低分', compute='_compute_stats') + score_detail_ids = fields.One2many('course.student.score.detail', 'score_item_id', string='成绩明细') + + def action_toggle_active(self): + for record in self: + record.is_active = not record.is_active + + def _compute_stats(self): + for record in self: + details = self.env['course.student.score.detail'].search([ + ('score_item_id', '=', record.id) + ]) + scores = details.mapped('score') + record.student_count = len(details) + record.avg_score = sum(scores) / len(scores) if scores else 0 + record.max_score = max(scores) if scores else 0 + record.min_score = min(scores) if scores else 0 \ No newline at end of file diff --git a/learning_center/models/course_student_score.py b/learning_center/models/course_student_score.py new file mode 100644 index 0000000..e01ba2e --- /dev/null +++ b/learning_center/models/course_student_score.py @@ -0,0 +1,112 @@ +from odoo import api, fields, models +from odoo.exceptions import ValidationError +class CourseStudentScore(models.Model): + _name = 'course.student.score' + _description = '学生成绩' + _rec_name = 'display_name' + _order = 'course_id, student_id' + _inherit = ['mail.thread'] + display_name = fields.Char(string='显示名称', compute='_compute_display_name', store=True) + + @api.depends('course_name', 'stu_name') + def _compute_display_name(self): + for record in self: + record.display_name = f"{record.stu_name} - {record.course_name}" + + course_id = fields.Many2one('learning.course', string='课程', required=True, ondelete='cascade') + # 课程扩展只读字段(替代视图嵌套写法) + course_code = fields.Char(string="课程代码", related="course_id.code", readonly=True) + course_credit = fields.Float(string="学分", related="course_id.credit", readonly=True) + course_exam_type = fields.Selection(string="考核方式", related="course_id.exam_type", readonly=True) + course_name = fields.Char(related='course_id.name', string='课程', required=True, ondelete='cascade') + student_id = fields.Many2one('student.info', string='学生', required=True) + student_no = fields.Char(string="学号", related="student_id.stu_num", readonly=True) + student_class_id = fields.Many2one( string="班级", related="student_id.class_id", readonly=True) + student_major_id = fields.Many2one( string="专业", related="student_id.major_id", readonly=True) + stu_name=fields.Char(related='student_id.stu_name',required=True, ondelete='cascade',string='学生姓名') + score_ids = fields.One2many('course.student.score.detail', 'student_score_id', string='成绩明细') + teaching_class_id=fields.Many2one('course.teaching_class',string='教学班') + # 教学班相关只读关联字段 + teaching_class_name = fields.Char(string="教学班名称", related="teaching_class_id.display_name", readonly=True) + teaching_class_semester = fields.Selection(string="学期", related="teaching_class_id.semester", readonly=True) + teaching_class_main_teacher = fields.Many2one('hr.employee', string="主讲教师", + related="teaching_class_id.main_teacher_id", readonly=True) + total_score = fields.Float(string='综合成绩', compute='_compute_total_score', store=True) + + grade_level = fields.Selection([ + ('A', '优秀(A)'), + ('B', '良好(B)'), + ('C', '中等(C)'), + ('D', '及格(D)'), + ('F', '不及格(F)'), + ], string='等级', compute='_compute_total_score', store=True) + + @api.depends('score_ids.score', 'score_ids.weight') + def _compute_total_score(self): + for record in self: + total = 0.0 + for detail in record.score_ids: + if detail.score and detail.weight: + total += detail.score * detail.weight / 100 + record.total_score = round(total, 2) + + if record.total_score >= 90: + record.grade_level = 'A' + elif record.total_score >= 80: + record.grade_level = 'B' + elif record.total_score >= 70: + record.grade_level = 'C' + elif record.total_score >= 60: + record.grade_level = 'D' + else: + record.grade_level = 'F' + + +class CourseStudentScoreDetail(models.Model): + _name = 'course.student.score.detail' + _description = '学生成绩明细' + _rec_name = 'display_name' + _order = 'student_score_id, score_item_id' + _inherit = ['mail.thread'] + display_name = fields.Char(string='显示名称', compute='_compute_display_name', store=True) + @api.depends('student_score_displayname', 'score_item_id.name') + def _compute_display_name(self): + for record in self: + record.display_name = f"{record.student_score_displayname} - {record.score_item_id.name}" + # 关联学生成绩主表 + student_score_id = fields.Many2one('course.student.score', string='学生成绩', required=True, ondelete='cascade') + student_score_displayname=fields.Char(related='student_score_id.display_name',required=True, ondelete='cascade',string='显示名称') + # 关联成绩项目 + score_item_id = fields.Many2one('course.score.item', string='成绩项目', required=True, ondelete='restrict') + + # 得分 + score = fields.Float(string='得分', help='该项目实际得分') + + # 从成绩项目继承的字段(用于显示和计算) + weight = fields.Float(string='权重(%)', related='score_item_id.weight', store=True, readonly=True) + full_score = fields.Float(string='满分', related='score_item_id.full_score', store=True, readonly=True) + score_type = fields.Selection(string='成绩类型', related='score_item_id.score_type', store=True, readonly=True) + # 备注 + remark = fields.Char(string='备注') + # 关联课程和学生(用于视图显示) + course_id = fields.Many2one('learning.course', string='课程', related='student_score_id.course_id', store=True, + readonly=True) + student_id = fields.Many2one('student.info', string='学生', related='student_score_id.student_id', store=True, + readonly=True) + # 完成率(得分/满分) + completion_rate = fields.Float(string='完成率(%)', compute='_compute_completion_rate', store=True) + @api.depends('score', 'full_score') + def _compute_completion_rate(self): + for record in self: + if record.full_score and record.score: + record.completion_rate = round(record.score / record.full_score * 100, 1) + else: + record.completion_rate = 0.0 + + @api.constrains('score', 'full_score') + def _check_score(self): + for record in self: + if record.score and record.full_score and record.score > record.full_score: + raise ValidationError(f'得分({record.score})不能超过满分({record.full_score})') + if record.score and record.score < 0: + raise ValidationError('得分不能为负数') \ No newline at end of file diff --git a/learning_center/models/course_syllabus.py b/learning_center/models/course_syllabus.py new file mode 100644 index 0000000..0d0a861 --- /dev/null +++ b/learning_center/models/course_syllabus.py @@ -0,0 +1,80 @@ +from odoo import api, fields, models +from odoo.tools import human_size +import base64 +import os + + +class CourseSyllabus(models.Model): + _name = 'course.syllabus' + _description = '教学大纲/教案' + _rec_name = 'title' + _order = 'course_id, syllabus_type, version desc' + _inherit = ['mail.thread'] + + title = fields.Char(string='标题', required=True) + syllabus_type = fields.Selection([ + ('syllabus', '教学大纲'), + ('lesson_plan', '教案'), + ], string='类型', required=True) + course_id = fields.Many2one('learning.course', string='所属课程', required=True, ondelete='cascade') + course_name = fields.Char(related="course_id.name", string="课程名称", readonly=True) + course_code = fields.Char(related="course_id.code", string="课程代码", readonly=True) + course_credit = fields.Float(related="course_id.credit", string="学分", readonly=True) + course_exam_type = fields.Selection(related="course_id.exam_type", string="考核方式", readonly=True) + chapter_id = fields.Many2one('course.chapter', string='关联章节') + chapter_name = fields.Char(related="chapter_id.name", string="章节名称", readonly=True) + chapter_sequence = fields.Integer(related="chapter_id.sequence", string="章节序号", readonly=True) + chapter_is_published = fields.Boolean(related="chapter_id.is_published", string="发布状态", readonly=True) + + # 文件核心字段 + file = fields.Binary(string='文件', attachment=True, required=True) + file_name = fields.Char(string='文件名') + file_size = fields.Integer(string='文件大小(字节)', readonly=True) + file_size_str = fields.Char(string='文件大小', readonly=True) # 友好显示:KB/MB + file_suffix = fields.Char(string='文件格式', readonly=True) # 后缀:docx/pptx/pdf + + version = fields.Char(string='版本号', default='1.0') + version_note = fields.Char(string='版本说明') + publish_date = fields.Date(string='发布日期', default=fields.Date.today) + is_published = fields.Boolean(string='是否发布', default=True) + download_count = fields.Integer(string='下载次数', default=0) + teaching_class_id = fields.Many2one( + 'course.teaching_class', + string='教学班', + ondelete='cascade', + domain="[('course_id', '=', course_id)]" + ) + + # 监听文件上传,自动赋值文件名、大小、后缀 + @api.onchange('file', 'file_name') + def _onchange_file_upload(self): + for record in self: + if record.file and record.file_name: + # 计算文件大小(base64 换算) + file_data = base64.b64decode(record.file) + size_bytes = len(file_data) + record.file_size = size_bytes + record.file_size_str = human_size(size_bytes) + + # 提取文件后缀 + name, suffix = os.path.splitext(record.file_name) + record.file_suffix = suffix.lower().strip('.') + else: + record.file_size = 0 + record.file_size_str = '' + record.file_suffix = '' + + # 修复批量下载问题 + 优化下载响应 + def action_download(self): + for rec in self: + rec.download_count += 1 + if rec.file_name: + # 携带真实文件名,浏览器正常识别 + url = f'/web/content/course.syllabus/{rec.id}/file/{rec.file_name}?download=true' + else: + url = f'/web/content/course.syllabus/{rec.id}/file?download=true' + return { + 'type': 'ir.actions.act_url', + 'url': url, + 'target': 'self', # self 强制下载,new 新标签页打开 + } \ No newline at end of file diff --git a/learning_center/models/course_teaching_class.py b/learning_center/models/course_teaching_class.py new file mode 100644 index 0000000..515af67 --- /dev/null +++ b/learning_center/models/course_teaching_class.py @@ -0,0 +1,291 @@ +from odoo import api, fields, models +from odoo.exceptions import ValidationError +import base64 +from io import BytesIO + +try: + import pandas as pd +except ImportError: + pd = None + + +class CourseTeachingClass(models.Model): + _name = 'course.teaching_class' + _description = '教学班' + _rec_name = 'display_name' + _order = 'course_id, semester, name' + _inherit = ['mail.thread', 'mail.activity.mixin'] + + # ==================== 显示名称 ==================== + display_name = fields.Char(string='显示名称', compute='_compute_display_name', store=True) + name = fields.Char(string='教学班名称', required=True, help='如:1班、2班、周一班') + code = fields.Char(string='班级代码', help='如:CS101-01') + semester = fields.Selection([ + ('2024-2025-1', '2024-2025学年第一学期'), + ('2024-2025-2', '2024-2025学年第二学期'), + ('2025-2026-1', '2025-2026学年第一学期'), + ('2025-2026-2', '2025-2026学年第二学期'), + ], string='学期', required=True, default=lambda self: self._get_current_semester()) + + # ==================== 关联课程 ==================== + course_id = fields.Many2one('learning.course', string='课程', required=True, ondelete='cascade') + course_name = fields.Char(related='course_id.name', string='课程名称') + + @api.depends('course_name', 'name', 'semester') + def _compute_display_name(self): + for record in self: + semester_map = dict(self._fields['semester'].selection) + semester_name = semester_map.get(record.semester, '') + record.display_name = f"{record.course_name} - {record.name}({semester_name})" + + # ==================== 基本信息 ==================== + course_code = fields.Char(string='课程代码', related='course_id.code', store=True) + course_credit = fields.Float(string='学分', related='course_id.credit', store=True) + course_exam_type = fields.Selection(string='考核方式', related='course_id.exam_type', store=True) + + # ==================== 教师信息 ==================== + teacher_ids = fields.Many2many('hr.employee', string='授课教师', + relation='teaching_class_teacher_rel') + main_teacher_id = fields.Many2one('hr.employee', string='主讲教师') + assistant_ids = fields.Many2many('hr.employee', string='助教', + relation='teaching_class_assistant_rel') + + # ==================== 学生信息(保留仅做展示,数据以 enrollment 为准) ==================== + student_ids = fields.Many2many('student.info', string='选课学生', + relation='teaching_class_student_rel') + student_count = fields.Integer(string='选课人数', compute='_compute_student_count') + + # ==================== 上课信息 ==================== + schedule = fields.Text(string='上课时间地点', help='如:周一 1-2节 教学楼101') + start_date = fields.Date(string='开课日期') + end_date = fields.Date(string='结课日期') + total_weeks = fields.Integer(string='教学周数', default=16) + + # ==================== 人数限制 ==================== + max_students = fields.Integer(string='人数上限', default=60) + min_students = fields.Integer(string='开课人数下限', default=20) + is_full = fields.Boolean(string='是否满员', compute='_compute_is_full') + + # ==================== 状态 ==================== + state = fields.Selection([ + ('draft', '未开放'), + ('open', '选课中'), + ('teaching', '教学中'), + ('closed', '已结课'), + ('cancelled', '已取消'), + ], string='状态', default='draft', tracking=True) + + # ==================== 教学进度 ==================== + current_chapter = fields.Integer(string='当前进度章节', help='当前教学进度') + progress_rate = fields.Float(string='教学进度(%)', compute='_compute_progress_rate') + + # ==================== 统计字段 ==================== + homework_count = fields.Integer(string='作业数量', compute='_compute_homework_count') + submitted_count = fields.Integer(string='已提交作业数', compute='_compute_homework_count') + resource_count = fields.Integer(string='资源数量', compute='_compute_resource_count') + + # ==================== 关联子表(核心:所有业务依赖 enrollment_ids) ==================== + homework_ids = fields.One2many('course.homework', 'teaching_class_id', string='班级作业') + student_score_ids = fields.One2many('course.student.score', 'teaching_class_id', string='班级成绩') + enrollment_ids = fields.One2many('course.teaching_class.enrollment', 'teaching_class_id', string='选课记录') + course_syllabus_ids = fields.One2many('course.syllabus', 'teaching_class_id', string='教案/大纲') + + # ==================== 计算方法(全部走选课记录) ==================== + def _compute_student_count(self): + for record in self: + valid_enroll = record.enrollment_ids.filtered(lambda e: e.state == 'enrolled') + record.student_count = len(valid_enroll) + + def _compute_is_full(self): + for record in self: + record.is_full = record.max_students > 0 and record.student_count >= record.max_students + + def _compute_progress_rate(self): + for record in self: + if record.total_weeks and record.current_chapter: + record.progress_rate = record.current_chapter / record.total_weeks * 100 + else: + record.progress_rate = 0 + + def _compute_homework_count(self): + for record in self: + homeworks = self.env['course.homework'].search([('teaching_class_id', '=', record.id)]) + record.homework_count = len(homeworks) + record.submitted_count = 0 + + def _compute_resource_count(self): + for record in self: + resources = self.env['course.chapter.resource'].search([ + ('teaching_class_id', '=', record.id) + ]) + record.resource_count = len(resources) + + # ==================== 业务按钮方法 ==================== + def action_open_enroll(self): + self.state = 'open' + + def action_start_teaching(self): + self.state = 'teaching' + + def action_close(self): + self.state = 'closed' + + def action_cancel(self): + self.state = 'cancelled' + + def action_add_students(self): + """打开批量添加学生向导(底层批量生成选课记录)""" + return { + 'type': 'ir.actions.act_window', + 'name': '添加学生', + 'res_model': 'course.teaching_class.enrollment.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': {'default_teaching_class_id': self.id}, + } + + def action_import_students(self): + """导入入口,复用向导""" + return self.action_add_students() + + @api.model + def _get_current_semester(self): + import datetime + now = datetime.datetime.now() + year = now.year + if now.month >= 9: + return f'{year}-{year + 1}-1' + elif now.month >= 2: + return f'{year - 1}-{year}-2' + else: + return f'{year - 1}-{year}-1' + + # 可选:双向同步 student_ids 和 enrollment_ids(保证两边显示一致,后期可删除 student_ids) + @api.depends('enrollment_ids.student_id') + def _sync_student_ids(self): + for rec in self: + stu_ids = rec.enrollment_ids.mapped('student_id').ids + rec.student_ids = [(6, 0, stu_ids)] + + student_ids = fields.Many2many( + 'student.info', + string='选课学生', + relation='teaching_class_student_rel', + compute='_sync_student_ids', + store=True + ) + + +class CourseTeachingClassEnrollment(models.Model): + _name = 'course.teaching_class.enrollment' + _description = '教学班选课记录' + _rec_name = 'display_name' + _order = 'teaching_class_id, student_id' + + display_name = fields.Char(string='显示名称', compute='_compute_display_name', store=True) + course_id = fields.Many2one(related="teaching_class_id.course_id", string="课程", store=True) + teaching_class_id = fields.Many2one('course.teaching_class', string='教学班', required=True, ondelete='cascade') + student_id = fields.Many2one('student.info', string='学生', required=True, ondelete='cascade') + stu_name = fields.Char(related='student_id.stu_name', string='学生姓名') + stu_phone = fields.Char(related='student_id.stu_phone', string='学生电话') + + # 选课信息 + enroll_date = fields.Date(string='选课日期', default=fields.Date.today) + state = fields.Selection([ + ('enrolled', '已选课'), + ('dropped', '已退课'), + ('failed', '不及格'), + ('passed', '已通过'), + ], string='状态', default='enrolled', tracking=True) + + # 成绩(最终成绩) + final_score = fields.Float(string='最终成绩') + grade_level = fields.Selection([ + ('A', '优秀'), + ('B', '良好'), + ('C', '中等'), + ('D', '及格'), + ('F', '不及格'), + ], string='等级') + + @api.depends('teaching_class_id.display_name', 'student_id.stu_name') + def _compute_display_name(self): + for record in self: + record.display_name = f"{record.student_id.stu_name} - {record.teaching_class_id.display_name}" + + @api.model + def create(self, vals): + """新增选课记录校验人数上限""" + res = super().create(vals) + cls = res.teaching_class_id + valid_num = len(cls.enrollment_ids.filtered(lambda e: e.state == 'enrolled')) + if cls.max_students and valid_num > cls.max_students: + raise ValidationError("该教学班人数已达上限,无法继续添加学生!") + return res + + +# ==================== 批量添加/导入学生 向导(临时模型) ==================== +class CourseTeachingClassEnrollmentWizard(models.TransientModel): + _name = 'course.teaching_class.enrollment.wizard' + _description = '批量添加&导入学生向导' + + teaching_class_id = fields.Many2one( + 'course.teaching_class', string='教学班', required=True + ) + student_ids = fields.Many2many('student.info', string='选择学生') + file = fields.Binary(string='上传Excel文件') + filename = fields.Char(string='文件名') + + def action_add_students(self): + """手动勾选学生 → 批量生成选课记录""" + self.ensure_one() + cls = self.teaching_class_id + exist_stu_ids = set(cls.enrollment_ids.mapped('student_id').ids) + create_list = [] + + for stu in self.student_ids: + if stu.id not in exist_stu_ids: + create_list.append({ + 'teaching_class_id': cls.id, + 'student_id': stu.id, + 'state': 'enrolled' + }) + + if create_list: + self.env['course.teaching_class.enrollment'].create(create_list) + return {'type': 'ir.actions.act_window_close'} + + def action_import_excel(self): + """Excel导入 → 批量生成选课记录""" + self.ensure_one() + if not self.file: + raise ValidationError("请先选择Excel文件!") + if not pd: + raise ValidationError("缺少 pandas 库,请先安装:pip install pandas openpyxl") + + cls = self.teaching_class_id + exist_stu_ids = set(cls.enrollment_ids.mapped('student_id').ids) + create_list = [] + + try: + file_data = base64.b64decode(self.file) + excel_file = BytesIO(file_data) + df = pd.read_excel(excel_file, engine='openpyxl') + + for _, row in df.iterrows(): + stu_code = str(row.get('学号', '')).strip() + if not stu_code: + continue + student = self.env['student.info'].search([('stu_id', '=', stu_code)], limit=1) + if student and student.id not in exist_stu_ids: + create_list.append({ + 'teaching_class_id': cls.id, + 'student_id': student.id, + 'state': 'enrolled' + }) + except Exception as e: + raise ValidationError(f"解析Excel失败:{str(e)}") + + if create_list: + self.env['course.teaching_class.enrollment'].create(create_list) + return {'type': 'ir.actions.act_window_close'} \ No newline at end of file diff --git a/learning_center/models/learning_course.py b/learning_center/models/learning_course.py new file mode 100644 index 0000000..b5f66b5 --- /dev/null +++ b/learning_center/models/learning_course.py @@ -0,0 +1,113 @@ +from odoo import api, fields, models + + +class LearningCourse(models.Model): + _name = 'learning.course' + _description = '课程' + _inherit = ['mail.thread', 'mail.activity.mixin'] + + name = fields.Char(string='课程名称', required=True) + code = fields.Char(string='课程代码', required=True, index=True) + english_name = fields.Char(string='英文名称') + credit = fields.Float(string='学分', required=True) + exam_type = fields.Selection([ + ('exam', '考试'), + ('assessment', '考查'), + ], string='考核方式') + description = fields.Html(string='课程简介') + objectives = fields.Html(string='教学目标') + state = fields.Selection([ + ('draft', '草稿'), + ('published', '已发布'), + ], default='draft', required=True) + # ==================== 关联章节 ==================== + chapter_ids = fields.One2many('course.chapter', 'course_id', string='章节') + chapter_count = fields.Integer(string='章节数', compute='_compute_chapter_count', store=True) + # ==================== 关联教学大纲/教案 ==================== + syllabus_ids = fields.One2many('course.syllabus', 'course_id', string='教学大纲/教案') + syllabus_count = fields.Integer(string='大纲/教案数', compute='_compute_syllabus_count', store=True) + # 按类型统计 + syllabus_count_total = fields.Integer(string='大纲总数', compute='_compute_syllabus_count') + lesson_plan_count = fields.Integer(string='教案总数', compute='_compute_syllabus_count') + # ==================== 统计资源(通过章节关联) ==================== + resource_count = fields.Integer(string='资源总数', compute='_compute_resource_count', store=True) + video_count = fields.Integer(string='视频数', compute='_compute_resource_count', store=True) + doc_count = fields.Integer(string='文档数', compute='_compute_resource_count', store=True) + + # 汇总所有章节的资源 + resource_all_ids = fields.One2many( + comodel_name='course.chapter.resource', + inverse_name=None, + compute='_compute_resource_all_ids', + string='资源列表', + readonly=True + ) + + @api.depends('chapter_ids', 'chapter_ids.resource_ids') + def _compute_resource_all_ids(self): + for record in self: + # 取出所有章节下的资源记录 + all_resources = record.chapter_ids.mapped('resource_ids') + record.resource_all_ids = all_resources + + # ==================== 计算字段方法 ==================== + @api.depends('chapter_ids') + def _compute_chapter_count(self): + for record in self: + record.chapter_count = len(record.chapter_ids) + + @api.depends('syllabus_ids', 'syllabus_ids.syllabus_type') + def _compute_syllabus_count(self): + for record in self: + record.syllabus_count = len(record.syllabus_ids) + record.syllabus_count_total = len(record.syllabus_ids.filtered(lambda s: s.syllabus_type == 'syllabus')) + record.lesson_plan_count = len(record.syllabus_ids.filtered(lambda s: s.syllabus_type == 'lesson_plan')) + + @api.depends('chapter_ids.resource_ids', 'chapter_ids.resource_ids.resource_type') + def _compute_resource_count(self): + for record in self: + resources = record.chapter_ids.mapped('resource_ids') + record.resource_count = len(resources) + record.video_count = len(resources.filtered(lambda r: r.resource_type == 'video')) + record.doc_count = len(resources.filtered(lambda r: r.resource_type in ['pdf', 'ppt'])) + + # ==================== 业务方法 ==================== + def action_publish(self): + """发布课程""" + self.state = 'published' + + def action_draft(self): + """退回草稿""" + self.state = 'draft' + + def action_view_chapters(self): + """查看章节""" + return { + 'type': 'ir.actions.act_window', + 'name': '课程章节', + 'res_model': 'course.chapter', + 'view_mode': 'list,form', + 'domain': [('course_id', '=', self.id)], + 'context': {'default_course_id': self.id}, + } + + def action_view_syllabus(self): + """查看教学大纲/教案""" + return { + 'type': 'ir.actions.act_window', + 'name': '教学大纲/教案', + 'res_model': 'course.syllabus', + 'view_mode': 'list,form', + 'domain': [('course_id', '=', self.id)], + 'context': {'default_course_id': self.id}, + } + + def action_view_resources(self): + """查看所有资源""" + return { + 'type': 'ir.actions.act_window', + 'name': '教学资源', + 'res_model': 'course.chapter.resource', + 'view_mode': 'list,form', + 'domain': [('chapter_id.course_id', '=', self.id)], + } \ No newline at end of file diff --git a/learning_center/models/teach_class.py b/learning_center/models/teach_class.py new file mode 100644 index 0000000..454f957 --- /dev/null +++ b/learning_center/models/teach_class.py @@ -0,0 +1,13 @@ +from odoo import api,fields,models + + + +class TeachClass(models.Model): + _name = 'teach.class' + course_teaching_class=fields.Many2one('course.teaching_class',string="教学班") + student_id=fields.Many2one('student.info',string='学生') + stu_num=fields.Char(related='student_id.stu_num',string='学号') + stu_name=fields.Char(related='student_id.stu_name',string='姓名') + stu_sex = fields.Selection(related='student_id.stu_sex', string="性别") + stu_phone = fields.Char(related='student_id.stu_phone', string="手机号") + stu_grade_xx = fields.Many2one(related='student_id.stu_grade_xx', string="年级") \ No newline at end of file diff --git a/learning_center/security/ir.model.access.csv b/learning_center/security/ir.model.access.csv new file mode 100644 index 0000000..41f39ec --- /dev/null +++ b/learning_center/security/ir.model.access.csv @@ -0,0 +1,24 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_learning_course_sys,learning.course.access.sys,model_learning_course,base.group_system,1,1,1,1 +access_course_chapter_sys,course.chapter.access.sys,model_course_chapter,base.group_system,1,1,1,1 +access_course_chapter_resource_sys,course.chapter.resource.access.sys,model_course_chapter_resource,base.group_system,1,1,1,1 +access_course_homework_sys,course.homework.access.sys,model_course_homework,base.group_system,1,1,1,1 +access_course_homework_submit_sys,course.homework.submit.access.sys,model_course_homework_submit,base.group_system,1,1,1,1 +access_course_score_item_sys,course.score.item.access.sys,model_course_score_item,base.group_system,1,1,1,1 +access_course_student_score_sys,course.student.score.access.sys,model_course_student_score,base.group_system,1,1,1,1 +access_course_student_score_detail_sys,course.student.score.detail.access.sys,model_course_student_score_detail,base.group_system,1,1,1,1 +access_course_syllabus_sys,course.syllabus.access.sys,model_course_syllabus,base.group_system,1,1,1,1 +access_course_teaching_class_sys,course.teaching.class.access.sys,model_course_teaching_class,base.group_system,1,1,1,1 +access_course_teaching_class_enrollment_sys,course.teaching.class.enrollment.access.sys,model_course_teaching_class_enrollment,base.group_system,1,1,1,1 +access_course_teaching_class_enrollment_wizard_sys,course.teaching.class.enrollment.access.sys,model_course_teaching_class_enrollment_wizard,base.group_system,1,1,1,1 +access_teach_class_teacher,teach.class.access.teacher,model_teach_class,edu_base.group_teacher,1,1,0,0 +access_teach_class_sys,teach.class.access.sys,model_teach_class,base.group_system,1,1,1,1 +access_course_teaching_class_teacher,course.teaching.class.access.teacher,model_course_teaching_class,edu_base.group_teacher,1,0,0,0 +access_course_teaching_class_enrollment_teacher,course.teaching.class.enrollment.access.teacher,model_course_teaching_class_enrollment,edu_base.group_teacher,1,0,0,0 +access_course_homework_teacher,course.homework.access.teacher,model_course_homework,edu_base.group_teacher,1,1,1,0 +access_course_homework_submit_teacher,course.homework.submit.access.teacher,model_course_homework_submit,edu_base.group_teacher,1,1,1,0 +access_learning_course_teacher,learning.course.access.teacher,model_learning_course,edu_base.group_teacher,1,0,0,0 +access_course_chapter_teacher,course.chapter.access.teacher,model_course_chapter,edu_base.group_teacher,1,1,1,0 +access_course_chapter_resource_teacher,course.chapter.resource.access.teacher,model_course_chapter_resource,edu_base.group_teacher,1,1,1,0 + + diff --git a/learning_center/security/record_rules.xml b/learning_center/security/record_rules.xml new file mode 100644 index 0000000..f4ff887 --- /dev/null +++ b/learning_center/security/record_rules.xml @@ -0,0 +1,41 @@ + + + + 教师-仅查看自己的班级数据 + + + + + [('main_teacher_id.user_id', '=', user.id)] + + + + + + + + 作业发布-仅查看自己发布的作业 + + + + + [('create_uid', '=', user.id)] + + + + + + + + 作业提价-仅查看自己发布的作业的提交情况 + + + + + [('homework_id.create_uid', '=', user.id)] + + + + + + \ No newline at end of file diff --git a/learning_center/views/view_course_chapter.xml b/learning_center/views/view_course_chapter.xml new file mode 100644 index 0000000..cdda94b --- /dev/null +++ b/learning_center/views/view_course_chapter.xml @@ -0,0 +1,135 @@ + + + course.chapter.list + course.chapter + + + + + + + + + + + + + + + course.chapter.form + course.chapter + +
+ +
+

+ +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + course.chapter.search + course.chapter + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + course.chapter.syllabus.sub.list + course.syllabus + + + + + + + + + + + + 章节管理 + course.chapter + list,form + + + + 章节管理 + + + + 20 + +
\ No newline at end of file diff --git a/learning_center/views/view_course_chapter_resource.xml b/learning_center/views/view_course_chapter_resource.xml new file mode 100644 index 0000000..1222e09 --- /dev/null +++ b/learning_center/views/view_course_chapter_resource.xml @@ -0,0 +1,107 @@ + + + course.chapter.resource.list + course.chapter.resource + + + + + + + + + + + + + + course.chapter.resource.form + course.chapter.resource + +
+ +
+

+ +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + 教学资源 + course.chapter.resource + list,form + + + + 教学资源 + + + + 30 + +
\ No newline at end of file diff --git a/learning_center/views/view_course_homework.xml b/learning_center/views/view_course_homework.xml new file mode 100644 index 0000000..28cf3db --- /dev/null +++ b/learning_center/views/view_course_homework.xml @@ -0,0 +1,123 @@ + + + course.homework.list + course.homework + + + + + + + + + + + + + + + + + course.homework.form + course.homework + +
+ +
+

+ +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + 作业管理 + course.homework + list,form + + + + 作业发布 + + + + 40 + +
diff --git a/learning_center/views/view_course_homework_student.xml b/learning_center/views/view_course_homework_student.xml new file mode 100644 index 0000000..16199d4 --- /dev/null +++ b/learning_center/views/view_course_homework_student.xml @@ -0,0 +1,103 @@ + + + + + + course.homework.student.list + course.homework + + + + + + + + + + + + + + + + course.homework.student.form + course.homework + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + +
+
+
+
+
+ + + + 我的作业 + course.homework + list,form + + + + + + + 学生作业 + 20 + + + + + + + + + + + + +
\ No newline at end of file diff --git a/learning_center/views/view_course_score_item.xml b/learning_center/views/view_course_score_item.xml new file mode 100644 index 0000000..24c9ed7 --- /dev/null +++ b/learning_center/views/view_course_score_item.xml @@ -0,0 +1,119 @@ + + + course.score.item.list + course.score.item + + + + + + + + + + + + + + + course.score.item.form + course.score.item + +
+ +
+

+ +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + course.student.score.detail.list + course.student.score.detail + + + + + + + + + + + 成绩项目 + course.score.item + list,form + + + + 成绩项目 + course.score.item + list,form + + + + + + + 成绩项目 + + + 50 + +
\ No newline at end of file diff --git a/learning_center/views/view_course_student_score.xml b/learning_center/views/view_course_student_score.xml new file mode 100644 index 0000000..4b7f81b --- /dev/null +++ b/learning_center/views/view_course_student_score.xml @@ -0,0 +1,204 @@ + + + + + + course.student.score.list + course.student.score + + + + + + + + + + + + + + + + course.student.score.form + course.student.score + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + course.student.score.detail.list + course.student.score.detail + + + + + + + + + + + + + + + + + course.student.score.detail.form + course.student.score.detail + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + 学生成绩 + course.student.score + list,form + + + + + + 成绩明细 + course.student.score.detail + list,form + + + + + + 学生成绩 + course.student.score + list,form + + [('course_id', '=', active_id)] + {'default_course_id': active_id} + + + + + 学生成绩 + course.student.score + list,form + + [('teaching_class_id', '=', active_id)] + {'default_teaching_class_id': active_id} + + + + + 学生成绩 + + + 60 + + + + + 成绩明细 + + + 65 + + +
\ No newline at end of file diff --git a/learning_center/views/view_course_syllabus.xml b/learning_center/views/view_course_syllabus.xml new file mode 100644 index 0000000..8ddf2f0 --- /dev/null +++ b/learning_center/views/view_course_syllabus.xml @@ -0,0 +1,134 @@ + + + + course.syllabus.list + course.syllabus + + + + + + + + + + + + + +