module_blog.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. package modules
  2. import (
  3. "html"
  4. "net/http"
  5. "strings"
  6. "golang-fave/assets"
  7. "golang-fave/consts"
  8. "golang-fave/engine/builder"
  9. "golang-fave/engine/fetdata"
  10. "golang-fave/engine/sqlw"
  11. "golang-fave/engine/wrapper"
  12. "golang-fave/utils"
  13. )
  14. func (this *Modules) RegisterModule_Blog() *Module {
  15. return this.newModule(MInfo{
  16. WantDB: true,
  17. Mount: "blog",
  18. Name: "Blog",
  19. Order: 1,
  20. System: false,
  21. Icon: assets.SysSvgIconList,
  22. Sub: &[]MISub{
  23. {Mount: "default", Name: "List of posts", Show: true, Icon: assets.SysSvgIconList},
  24. {Mount: "add", Name: "Add new post", Show: true, Icon: assets.SysSvgIconPlus},
  25. {Mount: "modify", Name: "Modify post", Show: false},
  26. {Sep: true, Show: true},
  27. {Mount: "categories", Name: "List of categories", Show: true, Icon: assets.SysSvgIconList},
  28. {Mount: "categories-add", Name: "Add new category", Show: true, Icon: assets.SysSvgIconPlus},
  29. {Mount: "categories-modify", Name: "Modify category", Show: false},
  30. },
  31. }, func(wrap *wrapper.Wrapper) {
  32. if len(wrap.UrlArgs) == 3 && wrap.UrlArgs[0] == "blog" && wrap.UrlArgs[1] == "category" && wrap.UrlArgs[2] != "" {
  33. // Blog category
  34. row := &utils.MySql_blog_category{}
  35. err := wrap.DB.QueryRow(`
  36. SELECT
  37. main.id,
  38. main.user,
  39. main.name,
  40. main.alias,
  41. main.lft,
  42. main.rgt,
  43. main.depth,
  44. parent.id AS parent_id
  45. FROM
  46. (
  47. SELECT
  48. node.id,
  49. node.user,
  50. node.name,
  51. node.alias,
  52. node.lft,
  53. node.rgt,
  54. (COUNT(parent.id) - 1) AS depth
  55. FROM
  56. blog_cats AS node,
  57. blog_cats AS parent
  58. WHERE
  59. node.lft BETWEEN parent.lft AND parent.rgt
  60. GROUP BY
  61. node.id
  62. ORDER BY
  63. node.lft ASC
  64. ) AS main
  65. LEFT JOIN (
  66. SELECT
  67. node.id,
  68. node.user,
  69. node.name,
  70. node.alias,
  71. node.lft,
  72. node.rgt,
  73. (COUNT(parent.id) - 0) AS depth
  74. FROM
  75. blog_cats AS node,
  76. blog_cats AS parent
  77. WHERE
  78. node.lft BETWEEN parent.lft AND parent.rgt
  79. GROUP BY
  80. node.id
  81. ORDER BY
  82. node.lft ASC
  83. ) AS parent ON
  84. parent.depth = main.depth AND
  85. main.lft > parent.lft AND
  86. main.rgt < parent.rgt
  87. WHERE
  88. main.id > 1 AND
  89. main.alias = ?
  90. ORDER BY
  91. main.lft ASC
  92. ;`,
  93. wrap.UrlArgs[2],
  94. ).Scan(
  95. &row.A_id,
  96. &row.A_user,
  97. &row.A_name,
  98. &row.A_alias,
  99. &row.A_lft,
  100. &row.A_rgt,
  101. &row.A_depth,
  102. &row.A_parent,
  103. )
  104. if err != nil && err != wrapper.ErrNoRows {
  105. // System error 500
  106. utils.SystemErrorPageEngine(wrap.W, err)
  107. return
  108. } else if err == wrapper.ErrNoRows {
  109. // User error 404 page
  110. wrap.RenderFrontEnd("404", fetdata.New(wrap, nil, true), http.StatusNotFound)
  111. return
  112. }
  113. // Fix url
  114. if wrap.R.URL.Path[len(wrap.R.URL.Path)-1] != '/' {
  115. http.Redirect(wrap.W, wrap.R, wrap.R.URL.Path+"/"+utils.ExtractGetParams(wrap.R.RequestURI), 301)
  116. return
  117. }
  118. // Render template
  119. wrap.RenderFrontEnd("blog-category", fetdata.New(wrap, row, false), http.StatusOK)
  120. return
  121. } else if len(wrap.UrlArgs) == 2 && wrap.UrlArgs[0] == "blog" && wrap.UrlArgs[1] != "" {
  122. // Blog post
  123. row := &utils.MySql_blog_post{}
  124. err := wrap.DB.QueryRow(`
  125. SELECT
  126. id,
  127. user,
  128. name,
  129. alias,
  130. category,
  131. briefly,
  132. content,
  133. UNIX_TIMESTAMP(datetime) as datetime,
  134. active
  135. FROM
  136. blog_posts
  137. WHERE
  138. active = 1 and
  139. alias = ?
  140. LIMIT 1;`,
  141. wrap.UrlArgs[1],
  142. ).Scan(
  143. &row.A_id,
  144. &row.A_user,
  145. &row.A_name,
  146. &row.A_alias,
  147. &row.A_category,
  148. &row.A_briefly,
  149. &row.A_content,
  150. &row.A_datetime,
  151. &row.A_active,
  152. )
  153. if err != nil && err != wrapper.ErrNoRows {
  154. // System error 500
  155. utils.SystemErrorPageEngine(wrap.W, err)
  156. return
  157. } else if err == wrapper.ErrNoRows {
  158. // User error 404 page
  159. wrap.RenderFrontEnd("404", fetdata.New(wrap, nil, true), http.StatusNotFound)
  160. return
  161. }
  162. // Fix url
  163. if wrap.R.URL.Path[len(wrap.R.URL.Path)-1] != '/' {
  164. http.Redirect(wrap.W, wrap.R, wrap.R.URL.Path+"/"+utils.ExtractGetParams(wrap.R.RequestURI), 301)
  165. return
  166. }
  167. // Render template
  168. wrap.RenderFrontEnd("blog-post", fetdata.New(wrap, row, false), http.StatusOK)
  169. return
  170. } else if len(wrap.UrlArgs) == 1 && wrap.UrlArgs[0] == "blog" {
  171. // Blog
  172. // Fix url
  173. if wrap.R.URL.Path[len(wrap.R.URL.Path)-1] != '/' {
  174. http.Redirect(wrap.W, wrap.R, wrap.R.URL.Path+"/"+utils.ExtractGetParams(wrap.R.RequestURI), 301)
  175. return
  176. }
  177. // Render template
  178. wrap.RenderFrontEnd("blog", fetdata.New(wrap, nil, false), http.StatusOK)
  179. return
  180. } else if (*wrap.Config).Engine.MainModule == 1 {
  181. // Render template
  182. wrap.RenderFrontEnd("blog", fetdata.New(wrap, nil, false), http.StatusOK)
  183. return
  184. }
  185. // User error 404 page
  186. wrap.RenderFrontEnd("404", fetdata.New(wrap, nil, true), http.StatusNotFound)
  187. }, func(wrap *wrapper.Wrapper) (string, string, string) {
  188. content := ""
  189. sidebar := ""
  190. if wrap.CurrSubModule == "" || wrap.CurrSubModule == "default" {
  191. content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
  192. {Name: "List of posts"},
  193. })
  194. content += builder.DataTable(
  195. wrap,
  196. "blog_posts",
  197. "id",
  198. "DESC",
  199. &[]builder.DataTableRow{
  200. {
  201. DBField: "id",
  202. },
  203. {
  204. DBField: "name",
  205. NameInTable: "Post / URL",
  206. CallBack: func(values *[]string) string {
  207. name := `<a href="/cp/` + wrap.CurrModule + `/modify/` + (*values)[0] + `/">` + html.EscapeString((*values)[1]) + `</a>`
  208. alias := html.EscapeString((*values)[2])
  209. return `<div>` + name + `</div><div><small>/blog/` + alias + `/</small></div>`
  210. },
  211. },
  212. {
  213. DBField: "alias",
  214. },
  215. {
  216. DBField: "datetime",
  217. DBExp: "UNIX_TIMESTAMP(`datetime`)",
  218. NameInTable: "Date / Time",
  219. Classes: "d-none d-md-table-cell",
  220. CallBack: func(values *[]string) string {
  221. t := int64(utils.StrToInt((*values)[3]))
  222. return `<div>` + utils.UnixTimestampToFormat(t, "02.01.2006") + `</div>` +
  223. `<div><small>` + utils.UnixTimestampToFormat(t, "15:04:05") + `</small></div>`
  224. },
  225. },
  226. {
  227. DBField: "active",
  228. NameInTable: "Active",
  229. Classes: "d-none d-sm-table-cell",
  230. CallBack: func(values *[]string) string {
  231. return builder.CheckBox(utils.StrToInt((*values)[4]))
  232. },
  233. },
  234. },
  235. func(values *[]string) string {
  236. return builder.DataTableAction(&[]builder.DataTableActionRow{
  237. {
  238. Icon: assets.SysSvgIconView,
  239. Href: `/blog/` + (*values)[2] + `/`,
  240. Hint: "View",
  241. Target: "_blank",
  242. },
  243. {
  244. Icon: assets.SysSvgIconEdit,
  245. Href: "/cp/" + wrap.CurrModule + "/modify/" + (*values)[0] + "/",
  246. Hint: "Edit",
  247. },
  248. {
  249. Icon: assets.SysSvgIconRemove,
  250. Href: "javascript:fave.ActionDataTableDelete(this,'blog-delete','" +
  251. (*values)[0] + "','Are you sure want to delete post?');",
  252. Hint: "Delete",
  253. Classes: "delete",
  254. },
  255. })
  256. },
  257. "/cp/"+wrap.CurrModule+"/",
  258. nil,
  259. nil,
  260. true,
  261. )
  262. } else if wrap.CurrSubModule == "categories" {
  263. content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
  264. {Name: "Categories", Link: "/cp/" + wrap.CurrModule + "/" + wrap.CurrSubModule + "/"},
  265. {Name: "List of categories"},
  266. })
  267. content += builder.DataTable(
  268. wrap,
  269. "blog_cats",
  270. "id",
  271. "ASC",
  272. &[]builder.DataTableRow{
  273. {
  274. DBField: "id",
  275. },
  276. {
  277. DBField: "user",
  278. },
  279. {
  280. DBField: "name",
  281. NameInTable: "Category",
  282. CallBack: func(values *[]string) string {
  283. depth := utils.StrToInt((*values)[4]) - 1
  284. if depth < 0 {
  285. depth = 0
  286. }
  287. sub := strings.Repeat("&mdash; ", depth)
  288. name := `<a href="/cp/` + wrap.CurrModule + `/categories-modify/` + (*values)[0] + `/">` + sub + html.EscapeString((*values)[2]) + `</a>`
  289. return `<div>` + name + `</div>`
  290. },
  291. },
  292. {
  293. DBField: "alias",
  294. },
  295. {
  296. DBField: "depth",
  297. },
  298. },
  299. func(values *[]string) string {
  300. return builder.DataTableAction(&[]builder.DataTableActionRow{
  301. {
  302. Icon: assets.SysSvgIconView,
  303. Href: `/blog/category/` + (*values)[3] + `/`,
  304. Hint: "View",
  305. Target: "_blank",
  306. },
  307. {
  308. Icon: assets.SysSvgIconEdit,
  309. Href: "/cp/" + wrap.CurrModule + "/categories-modify/" + (*values)[0] + "/",
  310. Hint: "Edit",
  311. },
  312. {
  313. Icon: assets.SysSvgIconRemove,
  314. Href: "javascript:fave.ActionDataTableDelete(this,'blog-categories-delete','" +
  315. (*values)[0] + "','Are you sure want to delete category?');",
  316. Hint: "Delete",
  317. Classes: "delete",
  318. },
  319. })
  320. },
  321. "/cp/"+wrap.CurrModule+"/"+wrap.CurrSubModule+"/",
  322. nil,
  323. func(limit_offset int, pear_page int) (*sqlw.Rows, error) {
  324. return wrap.DB.Query(
  325. `SELECT
  326. node.id,
  327. node.user,
  328. node.name,
  329. node.alias,
  330. (COUNT(parent.id) - 1) AS depth
  331. FROM
  332. blog_cats AS node,
  333. blog_cats AS parent
  334. WHERE
  335. node.lft BETWEEN parent.lft AND parent.rgt AND
  336. node.id > 1
  337. GROUP BY
  338. node.id
  339. ORDER BY
  340. node.lft ASC
  341. ;`,
  342. )
  343. },
  344. false,
  345. )
  346. } else if wrap.CurrSubModule == "add" || wrap.CurrSubModule == "modify" {
  347. if wrap.CurrSubModule == "add" {
  348. content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
  349. {Name: "Add new post"},
  350. })
  351. } else {
  352. content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
  353. {Name: "Modify post"},
  354. })
  355. }
  356. data := utils.MySql_blog_post{
  357. A_id: 0,
  358. A_user: 0,
  359. A_name: "",
  360. A_alias: "",
  361. A_category: 0,
  362. A_content: "",
  363. A_datetime: 0,
  364. A_active: 0,
  365. }
  366. if wrap.CurrSubModule == "modify" {
  367. if len(wrap.UrlArgs) != 3 {
  368. return "", "", ""
  369. }
  370. if !utils.IsNumeric(wrap.UrlArgs[2]) {
  371. return "", "", ""
  372. }
  373. err := wrap.DB.QueryRow(`
  374. SELECT
  375. id,
  376. user,
  377. name,
  378. alias,
  379. category,
  380. briefly,
  381. content,
  382. active
  383. FROM
  384. blog_posts
  385. WHERE
  386. id = ?
  387. LIMIT 1;`,
  388. utils.StrToInt(wrap.UrlArgs[2]),
  389. ).Scan(
  390. &data.A_id,
  391. &data.A_user,
  392. &data.A_name,
  393. &data.A_alias,
  394. &data.A_category,
  395. &data.A_briefly,
  396. &data.A_content,
  397. &data.A_active,
  398. )
  399. if err != nil {
  400. return "", "", ""
  401. }
  402. }
  403. // All post current categories
  404. var selids []int
  405. if data.A_id > 0 {
  406. rows, err := wrap.DB.Query("SELECT category_id FROM blog_cat_post_rel WHERE post_id = ?;", data.A_id)
  407. if err == nil {
  408. defer rows.Close()
  409. values := make([]int, 1)
  410. scan := make([]interface{}, len(values))
  411. for i := range values {
  412. scan[i] = &values[i]
  413. }
  414. for rows.Next() {
  415. err = rows.Scan(scan...)
  416. if err == nil {
  417. selids = append(selids, int(values[0]))
  418. }
  419. }
  420. }
  421. }
  422. btn_caption := "Add"
  423. if wrap.CurrSubModule == "modify" {
  424. btn_caption = "Save"
  425. }
  426. content += builder.DataForm(wrap, []builder.DataFormField{
  427. {
  428. Kind: builder.DFKHidden,
  429. Name: "action",
  430. Value: "blog-modify",
  431. },
  432. {
  433. Kind: builder.DFKHidden,
  434. Name: "id",
  435. Value: utils.IntToStr(data.A_id),
  436. },
  437. {
  438. Kind: builder.DFKText,
  439. Caption: "Post name",
  440. Name: "name",
  441. Value: data.A_name,
  442. Required: true,
  443. Min: "1",
  444. Max: "255",
  445. },
  446. {
  447. Kind: builder.DFKText,
  448. Caption: "Post alias",
  449. Name: "alias",
  450. Value: data.A_alias,
  451. Hint: "Example: our-news",
  452. Max: "255",
  453. },
  454. {
  455. Kind: builder.DFKText,
  456. Caption: "Category",
  457. Name: "category",
  458. Value: "0",
  459. CallBack: func(field *builder.DataFormField) string {
  460. return `<div class="form-group n4">` +
  461. `<div class="row">` +
  462. `<div class="col-md-3">` +
  463. `<label for="lbl_category">Category</label>` +
  464. `</div>` +
  465. `<div class="col-md-9">` +
  466. `<div>` +
  467. `<select class="selectpicker form-control" id="lbl_category" name="category" data-live-search="true">` +
  468. `<option title="Nothing selected" value="0">&mdash;</option>` +
  469. this.blog_GetCategorySelectOptions(wrap, 0, data.A_category, []int{}) +
  470. `</select>` +
  471. `</div>` +
  472. `</div>` +
  473. `</div>` +
  474. `</div>`
  475. },
  476. },
  477. {
  478. Kind: builder.DFKText,
  479. Caption: "Categories",
  480. Name: "cats",
  481. Value: "0",
  482. CallBack: func(field *builder.DataFormField) string {
  483. return `<div class="form-group n5">` +
  484. `<div class="row">` +
  485. `<div class="col-md-3">` +
  486. `<label for="lbl_parent">Categories</label>` +
  487. `</div>` +
  488. `<div class="col-md-9">` +
  489. `<div>` +
  490. `<select class="selectpicker form-control" id="lbl_cats" name="cats[]" data-live-search="true" multiple>` +
  491. this.blog_GetCategorySelectOptions(wrap, 0, 0, selids) +
  492. `</select>` +
  493. `</div>` +
  494. `</div>` +
  495. `</div>` +
  496. `</div>`
  497. },
  498. },
  499. {
  500. Kind: builder.DFKTextArea,
  501. Caption: "Briefly",
  502. Name: "briefly",
  503. Value: data.A_briefly,
  504. Classes: "briefly wysiwyg",
  505. },
  506. {
  507. Kind: builder.DFKTextArea,
  508. Caption: "Post content",
  509. Name: "content",
  510. Value: data.A_content,
  511. Classes: "wysiwyg",
  512. },
  513. {
  514. Kind: builder.DFKCheckBox,
  515. Caption: "Active",
  516. Name: "active",
  517. Value: utils.IntToStr(data.A_active),
  518. },
  519. {
  520. Kind: builder.DFKSubmit,
  521. Value: btn_caption,
  522. Target: "add-edit-button",
  523. },
  524. })
  525. if wrap.CurrSubModule == "add" {
  526. sidebar += `<button class="btn btn-primary btn-sidebar" id="add-edit-button">Add</button>`
  527. } else {
  528. sidebar += `<button class="btn btn-primary btn-sidebar" id="add-edit-button">Save</button>`
  529. }
  530. } else if wrap.CurrSubModule == "categories-add" || wrap.CurrSubModule == "categories-modify" {
  531. if wrap.CurrSubModule == "categories-add" {
  532. content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
  533. {Name: "Categories", Link: "/cp/" + wrap.CurrModule + "/categories/"},
  534. {Name: "Add new category"},
  535. })
  536. } else {
  537. content += this.getBreadCrumbs(wrap, &[]consts.BreadCrumb{
  538. {Name: "Categories", Link: "/cp/" + wrap.CurrModule + "/categories/"},
  539. {Name: "Modify category"},
  540. })
  541. }
  542. data := utils.MySql_blog_category{
  543. A_id: 0,
  544. A_user: 0,
  545. A_name: "",
  546. A_alias: "",
  547. A_lft: 0,
  548. A_rgt: 0,
  549. }
  550. if wrap.CurrSubModule == "categories-modify" {
  551. if len(wrap.UrlArgs) != 3 {
  552. return "", "", ""
  553. }
  554. if !utils.IsNumeric(wrap.UrlArgs[2]) {
  555. return "", "", ""
  556. }
  557. err := wrap.DB.QueryRow(`
  558. SELECT
  559. id,
  560. user,
  561. name,
  562. alias,
  563. lft,
  564. rgt
  565. FROM
  566. blog_cats
  567. WHERE
  568. id = ?
  569. LIMIT 1;`,
  570. utils.StrToInt(wrap.UrlArgs[2]),
  571. ).Scan(
  572. &data.A_id,
  573. &data.A_user,
  574. &data.A_name,
  575. &data.A_alias,
  576. &data.A_lft,
  577. &data.A_rgt,
  578. )
  579. if err != nil {
  580. return "", "", ""
  581. }
  582. }
  583. btn_caption := "Add"
  584. if wrap.CurrSubModule == "categories-modify" {
  585. btn_caption = "Save"
  586. }
  587. parentId := 0
  588. if wrap.CurrSubModule == "categories-modify" {
  589. parentId = this.blog_GetCategoryParentId(wrap, data.A_id)
  590. }
  591. content += builder.DataForm(wrap, []builder.DataFormField{
  592. {
  593. Kind: builder.DFKHidden,
  594. Name: "action",
  595. Value: "blog-categories-modify",
  596. },
  597. {
  598. Kind: builder.DFKHidden,
  599. Name: "id",
  600. Value: utils.IntToStr(data.A_id),
  601. },
  602. {
  603. Kind: builder.DFKText,
  604. Caption: "Parent",
  605. Name: "parent",
  606. Value: "0",
  607. CallBack: func(field *builder.DataFormField) string {
  608. return `<div class="form-group n2">` +
  609. `<div class="row">` +
  610. `<div class="col-md-3">` +
  611. `<label for="lbl_parent">Parent</label>` +
  612. `</div>` +
  613. `<div class="col-md-9">` +
  614. `<div>` +
  615. `<select class="selectpicker form-control" id="lbl_parent" name="parent" data-live-search="true">` +
  616. `<option title="Nothing selected" value="0">&mdash;</option>` +
  617. this.blog_GetCategorySelectOptions(wrap, data.A_id, parentId, []int{}) +
  618. `</select>` +
  619. `</div>` +
  620. `</div>` +
  621. `</div>` +
  622. `</div>`
  623. },
  624. },
  625. {
  626. Kind: builder.DFKText,
  627. Caption: "Name",
  628. Name: "name",
  629. Value: data.A_name,
  630. Required: true,
  631. Min: "1",
  632. Max: "255",
  633. },
  634. {
  635. Kind: builder.DFKText,
  636. Caption: "Alias",
  637. Name: "alias",
  638. Value: data.A_alias,
  639. Hint: "Example: popular-posts",
  640. Max: "255",
  641. },
  642. {
  643. Kind: builder.DFKSubmit,
  644. Value: btn_caption,
  645. Target: "add-edit-button",
  646. },
  647. })
  648. if wrap.CurrSubModule == "categories-add" {
  649. sidebar += `<button class="btn btn-primary btn-sidebar" id="add-edit-button">Add</button>`
  650. } else {
  651. sidebar += `<button class="btn btn-primary btn-sidebar" id="add-edit-button">Save</button>`
  652. }
  653. }
  654. return this.getSidebarModules(wrap), content, sidebar
  655. })
  656. }