Browse Source

Merge branch 'main' into fix-security-issues

Charles Shen 1 year ago
parent
commit
e5fb8d87fe

+ 1 - 1
example/Dockerfile

@@ -1,4 +1,4 @@
-FROM golang:1.21.0-alpine as builder
+FROM golang:1.21.1-alpine as builder
 RUN apk update && apk add git gcc libc-dev sqlite sqlite-dev && rm -rf /var/cache/apk/*
 RUN apk update && apk add git gcc libc-dev sqlite sqlite-dev && rm -rf /var/cache/apk/*
 ARG GITHUB_TOKEN
 ARG GITHUB_TOKEN
 WORKDIR /qor5
 WORKDIR /qor5

+ 24 - 24
example/admin/data_init.go

@@ -84,7 +84,7 @@ func InitDB(db *gorm.DB, tables []string) {
 	}
 	}
 	// Seq
 	// Seq
 	for _, name := range tables {
 	for _, name := range tables {
-		if err := db.Exec(fmt.Sprintf("SELECT setval('%s_id_seq', (SELECT MAX(id)+1 FROM %s));", name, name)).Error; err != nil {
+		if err := db.Exec(fmt.Sprintf("SELECT setval('%s_id_seq', (SELECT max(id) FROM %s));", name, name)).Error; err != nil {
 			panic(err)
 			panic(err)
 		}
 		}
 	}
 	}
@@ -229,20 +229,20 @@ INSERT INTO public.page_builder_categories VALUES (1, '2023-03-03 06:21:07.78251
 -- Data for Name: page_builder_containers; Type: TABLE DATA; Schema: public; Owner: example
 -- Data for Name: page_builder_containers; Type: TABLE DATA; Schema: public; Owner: example
 --
 --
 
 
-INSERT INTO public.page_builder_containers VALUES (1, '2023-03-03 06:20:48.334178+00', '2023-03-03 06:20:48.334178+00', NULL, 1, 'tpl', 'Image', 1, 1, false, false, 'Image', 'International');
-INSERT INTO public.page_builder_containers VALUES (2, '2023-03-03 06:21:40.233601+00', '2023-03-03 06:21:40.233601+00', NULL, 1, '2023-03-03-v01', 'Header', 1, 1, false, false, 'Header', 'International');
-INSERT INTO public.page_builder_containers VALUES (3, '2023-03-03 06:21:42.275791+00', '2023-03-03 06:21:42.275791+00', '2023-03-03 06:21:54.868151+00', 1, '2023-03-03-v01', 'Header', 2, 2, false, false, 'Header', 'International');
-INSERT INTO public.page_builder_containers VALUES (4, '2023-03-03 06:21:58.674323+00', '2023-03-03 06:21:58.674323+00', NULL, 1, '2023-03-03-v01', 'Video Banner', 1, 2, false, false, 'Video Banner', 'International');
-INSERT INTO public.page_builder_containers VALUES (5, '2023-03-03 06:22:46.641959+00', '2023-03-03 06:22:46.641959+00', NULL, 1, '2023-03-03-v01', 'Heading', 1, 3, false, false, 'Heading', 'International');
-INSERT INTO public.page_builder_containers VALUES (6, '2023-03-03 06:23:12.458215+00', '2023-03-03 06:23:12.458215+00', NULL, 1, '2023-03-03-v01', 'BrandGrid', 1, 4, false, false, 'BrandGrid', 'International');
-INSERT INTO public.page_builder_containers VALUES (7, '2023-03-03 06:24:15.676928+00', '2023-03-03 06:24:15.676928+00', NULL, 1, '2023-03-03-v01', 'Heading', 2, 5, false, false, 'Heading', 'International');
-INSERT INTO public.page_builder_containers VALUES (8, '2023-03-03 06:24:40.374505+00', '2023-03-03 06:24:40.374505+00', NULL, 1, '2023-03-03-v01', 'ListContent', 1, 6, false, false, 'ListContent', 'International');
-INSERT INTO public.page_builder_containers VALUES (9, '2023-03-03 06:25:41.972811+00', '2023-03-03 06:25:41.972811+00', NULL, 1, '2023-03-03-v01', 'Image', 2, 7, false, false, 'Image', 'International');
-INSERT INTO public.page_builder_containers VALUES (10, '2023-03-03 06:25:55.874078+00', '2023-03-03 06:25:55.874078+00', NULL, 1, '2023-03-03-v01', 'Heading', 3, 8, false, false, 'Heading', 'International');
-INSERT INTO public.page_builder_containers VALUES (11, '2023-03-03 06:26:29.013538+00', '2023-03-03 06:26:29.013538+00', NULL, 1, '2023-03-03-v01', 'ListContent', 2, 9, false, false, 'ListContent', 'International');
-INSERT INTO public.page_builder_containers VALUES (12, '2023-03-03 06:27:24.174737+00', '2023-03-03 06:27:24.174737+00', NULL, 1, '2023-03-03-v01', 'InNumbers', 1, 10, false, false, 'InNumbers', 'International');
-INSERT INTO public.page_builder_containers VALUES (13, '2023-03-03 06:27:54.022522+00', '2023-03-03 06:28:27.625631+00', NULL, 1, '2023-03-03-v01', 'ContactForm', 1, 11, true, false, 'ContactForm', 'International');
-INSERT INTO public.page_builder_containers VALUES (14, '2023-03-03 06:28:30.305332+00', '2023-03-03 06:28:30.305332+00', NULL, 1, '2023-03-03-v01', 'Footer', 1, 12, false, false, 'Footer', 'International');
+INSERT INTO public.page_builder_containers VALUES (1, '2023-03-03 06:20:48.334178+00', '2023-03-03 06:20:48.334178+00', NULL, 1, 'tpl', 'Image', 1, 1, false, false, 'Image', 'International', 0);
+INSERT INTO public.page_builder_containers VALUES (2, '2023-03-03 06:21:40.233601+00', '2023-03-03 06:21:40.233601+00', NULL, 1, '2023-03-03-v01', 'Header', 1, 1, false, false, 'Header', 'International', 0);
+INSERT INTO public.page_builder_containers VALUES (3, '2023-03-03 06:21:42.275791+00', '2023-03-03 06:21:42.275791+00', '2023-03-03 06:21:54.868151+00', 1, '2023-03-03-v01', 'Header', 2, 2, false, false, 'Header', 'International', 0);
+INSERT INTO public.page_builder_containers VALUES (4, '2023-03-03 06:21:58.674323+00', '2023-03-03 06:21:58.674323+00', NULL, 1, '2023-03-03-v01', 'Video Banner', 1, 2, false, false, 'Video Banner', 'International', 0);
+INSERT INTO public.page_builder_containers VALUES (5, '2023-03-03 06:22:46.641959+00', '2023-03-03 06:22:46.641959+00', NULL, 1, '2023-03-03-v01', 'Heading', 1, 3, false, false, 'Heading', 'International', 0);
+INSERT INTO public.page_builder_containers VALUES (6, '2023-03-03 06:23:12.458215+00', '2023-03-03 06:23:12.458215+00', NULL, 1, '2023-03-03-v01', 'BrandGrid', 1, 4, false, false, 'BrandGrid', 'International', 0);
+INSERT INTO public.page_builder_containers VALUES (7, '2023-03-03 06:24:15.676928+00', '2023-03-03 06:24:15.676928+00', NULL, 1, '2023-03-03-v01', 'Heading', 2, 5, false, false, 'Heading', 'International', 0);
+INSERT INTO public.page_builder_containers VALUES (8, '2023-03-03 06:24:40.374505+00', '2023-03-03 06:24:40.374505+00', NULL, 1, '2023-03-03-v01', 'ListContent', 1, 6, false, false, 'ListContent', 'International', 0);
+INSERT INTO public.page_builder_containers VALUES (9, '2023-03-03 06:25:41.972811+00', '2023-03-03 06:25:41.972811+00', NULL, 1, '2023-03-03-v01', 'Image', 2, 7, false, false, 'Image', 'International', 0);
+INSERT INTO public.page_builder_containers VALUES (10, '2023-03-03 06:25:55.874078+00', '2023-03-03 06:25:55.874078+00', NULL, 1, '2023-03-03-v01', 'Heading', 3, 8, false, false, 'Heading', 'International', 0);
+INSERT INTO public.page_builder_containers VALUES (11, '2023-03-03 06:26:29.013538+00', '2023-03-03 06:26:29.013538+00', NULL, 1, '2023-03-03-v01', 'ListContent', 2, 9, false, false, 'ListContent', 'International', 0);
+INSERT INTO public.page_builder_containers VALUES (12, '2023-03-03 06:27:24.174737+00', '2023-03-03 06:27:24.174737+00', NULL, 1, '2023-03-03-v01', 'InNumbers', 1, 10, false, false, 'InNumbers', 'International', 0);
+INSERT INTO public.page_builder_containers VALUES (13, '2023-03-03 06:27:54.022522+00', '2023-03-03 06:28:27.625631+00', NULL, 1, '2023-03-03-v01', 'ContactForm', 1, 11, true, false, 'ContactForm', 'International', 0);
+INSERT INTO public.page_builder_containers VALUES (14, '2023-03-03 06:28:30.305332+00', '2023-03-03 06:28:30.305332+00', NULL, 1, '2023-03-03-v01', 'Footer', 1, 12, false, false, 'Footer', 'International', 0);
 
 
 
 
 --
 --
@@ -255,7 +255,7 @@ INSERT INTO public.page_builder_containers VALUES (14, '2023-03-03 06:28:30.3053
 -- Data for Name: page_builder_pages; Type: TABLE DATA; Schema: public; Owner: example
 -- Data for Name: page_builder_pages; Type: TABLE DATA; Schema: public; Owner: example
 --
 --
 
 
-INSERT INTO public.page_builder_pages VALUES (1, '2023-03-03 06:20:35.886165+00', '2023-03-03 06:20:35.886165+00', NULL, 'The Plant Homepage', '/', 0, 'draft', '', NULL, NULL, NULL, NULL, '2023-03-03-v01', '', '', 'International');
+INSERT INTO public.page_builder_pages VALUES (1, '2023-03-03 06:20:35.886165+00', '2023-03-03 06:20:35.886165+00', NULL, 'The Plant Homepage', '/', 0, 'draft', '', NULL, NULL, NULL, NULL, '2023-03-03-v01', '', '', '', 'International');
 
 
 
 
 --
 --
@@ -284,17 +284,17 @@ INSERT INTO public.qor_jobs VALUES (3, '2022-01-10 20:51:44.495127+09', '2022-01
 INSERT INTO public.qor_jobs VALUES (67, '2022-10-20 11:38:34.139332+09', '2022-10-20 11:38:39.247979+09', NULL, 'errorJob', 'exception');
 INSERT INTO public.qor_jobs VALUES (67, '2022-10-20 11:38:34.139332+09', '2022-10-20 11:38:39.247979+09', NULL, 'errorJob', 'exception');
 INSERT INTO public.qor_jobs VALUES (68, '2022-10-20 11:46:25.042928+09', '2022-10-20 11:46:30.094506+09', NULL, 'panicJob', 'exception');
 INSERT INTO public.qor_jobs VALUES (68, '2022-10-20 11:46:25.042928+09', '2022-10-20 11:46:30.094506+09', NULL, 'panicJob', 'exception');
 
 
-insert into public.qor_job_instances (id, created_at, updated_at, deleted_at, qor_job_id, job, status, args, progress, progress_text, operator, context) values (1, '2021-11-15 05:38:25.337004 +00:00', '2021-11-15 05:38:25.517271 +00:00', null, 1, 'noArgJob', 'done', 'null', 100, '', null, null);
-insert into public.qor_job_instances (id, created_at, updated_at, deleted_at, qor_job_id, job, status, args, progress, progress_text, operator, context) values (34, '2022-10-08 03:15:48.270563 +00:00', '2022-10-14 07:16:05.224650 +00:00', null, 34, 'scheduleJob', 'done', '{"F1":"f","ScheduleTime":"2022-10-14T07:16:00Z"}', 100, '', '', '{"URL":"https://example.qor5.theplant-dev.com/admin/workers"}');
-insert into public.qor_job_instances (id, created_at, updated_at, deleted_at, qor_job_id, job, status, args, progress, progress_text, operator, context) values (2, '2021-12-07 13:31:07.389003 +00:00', '2021-12-07 13:31:12.460350 +00:00', null, 2, 'progressTextJob', 'done', 'null', 100, '<a href="https://www.google.com">Download users</a>', null, null);
-insert into public.qor_job_instances (id, created_at, updated_at, deleted_at, qor_job_id, job, status, args, progress, progress_text, operator, context) values (3, '2022-01-10 11:51:44.506654 +00:00', '2022-01-10 11:51:44.631661 +00:00', null, 3, 'scheduleJob', 'done', '{"F1":"fda","ScheduleTime":null}', 100, '', null, null);
-insert into public.qor_job_instances (id, created_at, updated_at, deleted_at, qor_job_id, job, status, args, progress, progress_text, operator, context) values (67, '2022-10-20 02:38:34.152825 +00:00', '2022-10-20 02:38:39.251747 +00:00', null, 67, 'errorJob', 'exception', 'null', 0, 'imError', '', '{"URL":"https://example.qor5.theplant-dev.com/admin/workers"}');
-insert into public.qor_job_instances (id, created_at, updated_at, deleted_at, qor_job_id, job, status, args, progress, progress_text, operator, context) values (68, '2022-10-20 02:46:25.047450 +00:00', '2022-10-20 02:46:30.102953 +00:00', null, 68, 'panicJob', 'exception', 'null', 0, 'letsPanic', '', '{"URL":"https://example.qor5.theplant-dev.com/admin/workers"}');
+INSERT INTO public.qor_job_instances (id, created_at, updated_at, deleted_at, qor_job_id, job, status, args, progress, progress_text, operator, context) VALUES (1, '2021-11-15 05:38:25.337004 +00:00', '2021-11-15 05:38:25.517271 +00:00', NULL, 1, 'noArgJob', 'done', 'null', 100, '', NULL, NULL);
+INSERT INTO public.qor_job_instances (id, created_at, updated_at, deleted_at, qor_job_id, job, status, args, progress, progress_text, operator, context) VALUES (34, '2022-10-08 03:15:48.270563 +00:00', '2022-10-14 07:16:05.224650 +00:00', NULL, 34, 'scheduleJob', 'done', '{"F1":"f","ScheduleTime":"2022-10-14T07:16:00Z"}', 100, '', '', '{"URL":"https://example.qor5.theplant-dev.com/admin/workers"}');
+INSERT INTO public.qor_job_instances (id, created_at, updated_at, deleted_at, qor_job_id, job, status, args, progress, progress_text, operator, context) VALUES (2, '2021-12-07 13:31:07.389003 +00:00', '2021-12-07 13:31:12.460350 +00:00', NULL, 2, 'progressTextJob', 'done', 'null', 100, '<a href="https://www.google.com">Download users</a>', NULL, NULL);
+INSERT INTO public.qor_job_instances (id, created_at, updated_at, deleted_at, qor_job_id, job, status, args, progress, progress_text, operator, context) VALUES (3, '2022-01-10 11:51:44.506654 +00:00', '2022-01-10 11:51:44.631661 +00:00', NULL, 3, 'scheduleJob', 'done', '{"F1":"fda","ScheduleTime":null}', 100, '', NULL, NULL);
+INSERT INTO public.qor_job_instances (id, created_at, updated_at, deleted_at, qor_job_id, job, status, args, progress, progress_text, operator, context) VALUES (67, '2022-10-20 02:38:34.152825 +00:00', '2022-10-20 02:38:39.251747 +00:00', NULL, 67, 'errorJob', 'exception', 'null', 0, 'imError', '', '{"URL":"https://example.qor5.theplant-dev.com/admin/workers"}');
+INSERT INTO public.qor_job_instances (id, created_at, updated_at, deleted_at, qor_job_id, job, status, args, progress, progress_text, operator, context) VALUES (68, '2022-10-20 02:46:25.047450 +00:00', '2022-10-20 02:46:30.102953 +00:00', NULL, 68, 'panicJob', 'exception', 'null', 0, 'letsPanic', '', '{"URL":"https://example.qor5.theplant-dev.com/admin/workers"}');
 `
 `
 
 
 	initCategoriesSQL = `INSERT INTO "categories" ("created_at","updated_at","deleted_at","name","products","status","online_url","scheduled_start_at","scheduled_end_at","actual_start_at","actual_end_at","version_name","parent_version","version") VALUES ('2023-01-05 15:19:30.633','2023-01-05 15:19:30.633',NULL,'Demo',NULL,'draft','',NULL,NULL,NULL,NULL,'','','2023-01-05-v01') RETURNING "id","version"`
 	initCategoriesSQL = `INSERT INTO "categories" ("created_at","updated_at","deleted_at","name","products","status","online_url","scheduled_start_at","scheduled_end_at","actual_start_at","actual_end_at","version_name","parent_version","version") VALUES ('2023-01-05 15:19:30.633','2023-01-05 15:19:30.633',NULL,'Demo',NULL,'draft','',NULL,NULL,NULL,NULL,'','','2023-01-05-v01') RETURNING "id","version"`
 
 
-	initInputDemosSQL = `INSERT INTO "input_demos" ("text_field1","text_area1","switch1","slider1","select1","range_slider1","radio1","file_input1","combobox1","checkbox1","autocomplete1","button_group1","chip_group1","item_group1","list_item_group1","slide_group1","color_picker1","date_picker1","date_picker_month1","time_picker1","media_library1","updated_at","created_at") VALUES ('Demo','',false,0,'',NULL,'','','',false,'{""}','','','','','','','','','',NULL,'2023-01-05 15:21:36.488','2023-01-05 15:21:36.488') RETURNING "id"`
+	initInputDemosSQL = `INSERT INTO "input_demos" ("text_field1","text_area1","switch1","slider1","select1","range_slider1","radio1","file_input1","combobox1","checkbox1","autocomplete1","button_group1","chip_group1","item_group1","list_item_group1","slide_group1","color_picker1","date_picker1","date_picker_month1","time_picker1","media_library1","updated_at","created_at") VALUES ('Demo','',FALSE,0,'',NULL,'','','',FALSE,'{""}','','','','','','','','','',NULL,'2023-01-05 15:21:36.488','2023-01-05 15:21:36.488') RETURNING "id"`
 
 
 	initPostsSQL = `INSERT INTO "posts" ("created_at","updated_at","deleted_at","title","title_with_slug","seo","body","hero_image","body_image","status","online_url","scheduled_start_at","scheduled_end_at","actual_start_at","actual_end_at","version_name","parent_version","version") VALUES ('2023-01-05 15:23:55.553','2023-01-05 15:23:55.553',NULL,'Demo','demo','{"Title":"","Description":"","Keywords":"","OpenGraphURL":"","OpenGraphType":"","OpenGraphImageURL":"","OpenGraphImageFromMediaLibrary":{"ID":0,"Url":"","VideoLink":"","FileName":"","Description":""},"OpenGraphMetadata":null,"EnabledCustomize":false}','<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna. Nunc viverra imperdiet enim. Fusce est. Vivamus a tellus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Proin pharetra nonummy pede. Mauris et orci. Aenean nec lorem. In porttitor. Donec laoreet nonummy augue. Suspendisse dui purus, scelerisque at, vulputate vitae, pretium mattis, nunc. Mauris eget neque at sem venenatis eleifend. Ut nonummy.</p>','{"ID":1,"Url":"//qor5-test.s3.ap-northeast-1.amazonaws.com/system/media_libraries/1/file.jpeg","VideoLink":"","FileName":"demo image.jpeg","Description":"","FileSizes":{"@qor_preview":8917,"default":326350,"main":94913,"og":123973,"original":326350,"thumb":21199,"twitter-large":117784,"twitter-small":77615},"Width":750,"Height":1000}',NULL,'draft','',NULL,NULL,NULL,NULL,'','','2023-01-05-v01') RETURNING "id","version"`
 	initPostsSQL = `INSERT INTO "posts" ("created_at","updated_at","deleted_at","title","title_with_slug","seo","body","hero_image","body_image","status","online_url","scheduled_start_at","scheduled_end_at","actual_start_at","actual_end_at","version_name","parent_version","version") VALUES ('2023-01-05 15:23:55.553','2023-01-05 15:23:55.553',NULL,'Demo','demo','{"Title":"","Description":"","Keywords":"","OpenGraphURL":"","OpenGraphType":"","OpenGraphImageURL":"","OpenGraphImageFromMediaLibrary":{"ID":0,"Url":"","VideoLink":"","FileName":"","Description":""},"OpenGraphMetadata":null,"EnabledCustomize":false}','<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna. Nunc viverra imperdiet enim. Fusce est. Vivamus a tellus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Proin pharetra nonummy pede. Mauris et orci. Aenean nec lorem. In porttitor. Donec laoreet nonummy augue. Suspendisse dui purus, scelerisque at, vulputate vitae, pretium mattis, nunc. Mauris eget neque at sem venenatis eleifend. Ut nonummy.</p>','{"ID":1,"Url":"//qor5-test.s3.ap-northeast-1.amazonaws.com/system/media_libraries/1/file.jpeg","VideoLink":"","FileName":"demo image.jpeg","Description":"","FileSizes":{"@qor_preview":8917,"default":326350,"main":94913,"og":123973,"original":326350,"thumb":21199,"twitter-large":117784,"twitter-small":77615},"Width":750,"Height":1000}',NULL,'draft','',NULL,NULL,NULL,NULL,'','','2023-01-05-v01') RETURNING "id","version"`
 
 
@@ -308,7 +308,7 @@ INSERT INTO public.addresses VALUES (3, 1, 'Canberra 73/30 Lonsdale Street, Brad
 INSERT INTO public.membership_cards VALUES (1, 1, 0, NULL);
 INSERT INTO public.membership_cards VALUES (1, 1, 0, NULL);
 `
 `
 
 
-	initListModelsSQL = `INSERT INTO "list_models" ("created_at","updated_at","deleted_at","title","status","online_url","scheduled_start_at","scheduled_end_at","actual_start_at","actual_end_at","page_number","position","list_deleted","list_updated","version_name","parent_version","version") VALUES ('2023-01-05 17:45:36.783','2023-01-05 17:45:36.783',NULL,'Demo','draft','',NULL,NULL,NULL,NULL,0,0,false,false,'','','2023-01-05-v01') RETURNING "id","version"`
+	initListModelsSQL = `INSERT INTO "list_models" ("created_at","updated_at","deleted_at","title","status","online_url","scheduled_start_at","scheduled_end_at","actual_start_at","actual_end_at","page_number","position","list_deleted","list_updated","version_name","parent_version","version") VALUES ('2023-01-05 17:45:36.783','2023-01-05 17:45:36.783',NULL,'Demo','draft','',NULL,NULL,NULL,NULL,0,0,FALSE,FALSE,'','','2023-01-05-v01') RETURNING "id","version"`
 
 
 	initMicrositeModelsSQL = `INSERT INTO "microsite_models" ("name","description","created_at","updated_at","deleted_at","status","online_url","scheduled_start_at","scheduled_end_at","actual_start_at","actual_end_at","version_name","parent_version","pre_path","package","files_list","unix_key","version") VALUES ('Demo','','2023-01-05 17:49:45.695','2023-01-05 17:49:45.695',NULL,'draft','',NULL,NULL,NULL,NULL,'','','','{"FileName":"","Url":""}','','','2023-01-05-v01') RETURNING "id","version"`
 	initMicrositeModelsSQL = `INSERT INTO "microsite_models" ("name","description","created_at","updated_at","deleted_at","status","online_url","scheduled_start_at","scheduled_end_at","actual_start_at","actual_end_at","version_name","parent_version","pre_path","package","files_list","unix_key","version") VALUES ('Demo','','2023-01-05 17:49:45.695','2023-01-05 17:49:45.695',NULL,'draft','',NULL,NULL,NULL,NULL,'','','','{"FileName":"","Url":""}','','','2023-01-05-v01') RETURNING "id","version"`
 
 

+ 1 - 1
example/cmd/data-resetor/Dockerfile

@@ -1,4 +1,4 @@
-FROM golang:1.21.0-alpine as builder
+FROM golang:1.21.1-alpine as builder
 RUN apk update && apk add git gcc libc-dev && rm -rf /var/cache/apk/*
 RUN apk update && apk add git gcc libc-dev && rm -rf /var/cache/apk/*
 WORKDIR /qor5
 WORKDIR /qor5
 COPY . .
 COPY . .

+ 1 - 1
example/cmd/publisher/Dockerfile

@@ -1,4 +1,4 @@
-FROM golang:1.21.0-alpine as builder
+FROM golang:1.21.1-alpine as builder
 RUN apk update && apk add git gcc libc-dev sqlite sqlite-dev && rm -rf /var/cache/apk/*
 RUN apk update && apk add git gcc libc-dev sqlite sqlite-dev && rm -rf /var/cache/apk/*
 WORKDIR /qor5
 WORKDIR /qor5
 COPY . .
 COPY . .

+ 2 - 2
go.mod

@@ -25,7 +25,7 @@ require (
 	github.com/pquerna/otp v1.4.0
 	github.com/pquerna/otp v1.4.0
 	github.com/qor/oss v0.0.0-20230717083721-c04686f83630
 	github.com/qor/oss v0.0.0-20230717083721-c04686f83630
 	github.com/qor5/ui v1.0.1-0.20230913083355-743825ff29b1
 	github.com/qor5/ui v1.0.1-0.20230913083355-743825ff29b1
-	github.com/qor5/web v1.2.5-0.20230905084205-145cb859a65f
+	github.com/qor5/web v1.3.0
 	github.com/qor5/x v1.2.1-0.20230907054212-50b1a850acf6
 	github.com/qor5/x v1.2.1-0.20230907054212-50b1a850acf6
 	github.com/sunfmin/reflectutils v1.0.3
 	github.com/sunfmin/reflectutils v1.0.3
 	github.com/theplant/bimg v1.1.1
 	github.com/theplant/bimg v1.1.1
@@ -37,6 +37,7 @@ require (
 	github.com/tnclong/go-que v0.0.0-20201111043106-1fc5fa2b9761
 	github.com/tnclong/go-que v0.0.0-20201111043106-1fc5fa2b9761
 	github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f
 	github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f
 	github.com/wcharczuk/go-chart/v2 v2.1.0
 	github.com/wcharczuk/go-chart/v2 v2.1.0
+	go.uber.org/multierr v1.11.0
 	go.uber.org/zap v1.24.0
 	go.uber.org/zap v1.24.0
 	goji.io v2.0.2+incompatible
 	goji.io v2.0.2+incompatible
 	golang.org/x/text v0.9.0
 	golang.org/x/text v0.9.0
@@ -90,7 +91,6 @@ require (
 	github.com/therootcompany/xz v1.0.1 // indirect
 	github.com/therootcompany/xz v1.0.1 // indirect
 	github.com/ulikunitz/xz v0.5.11 // indirect
 	github.com/ulikunitz/xz v0.5.11 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
-	go.uber.org/multierr v1.11.0 // indirect
 	go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
 	go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
 	golang.org/x/crypto v0.9.0 // indirect
 	golang.org/x/crypto v0.9.0 // indirect
 	golang.org/x/image v0.7.0 // indirect
 	golang.org/x/image v0.7.0 // indirect

+ 2 - 2
go.sum

@@ -290,8 +290,8 @@ github.com/qor/oss v0.0.0-20230717083721-c04686f83630 h1:CRi4xF7B8aGX/y48NCjarNd
 github.com/qor/oss v0.0.0-20230717083721-c04686f83630/go.mod h1:FDxJAVwmZ1j8ITcKJExFlzkTYuUor1dBKZgNVWqEqlM=
 github.com/qor/oss v0.0.0-20230717083721-c04686f83630/go.mod h1:FDxJAVwmZ1j8ITcKJExFlzkTYuUor1dBKZgNVWqEqlM=
 github.com/qor5/ui v1.0.1-0.20230913083355-743825ff29b1 h1:6ZIyg13zG0ki2yE2XcFN20RkwCMUjIJkYHCGIIqrq5c=
 github.com/qor5/ui v1.0.1-0.20230913083355-743825ff29b1 h1:6ZIyg13zG0ki2yE2XcFN20RkwCMUjIJkYHCGIIqrq5c=
 github.com/qor5/ui v1.0.1-0.20230913083355-743825ff29b1/go.mod h1:bgBqjIytHRdfTsiZea8df/ltAcyQyuHiLbecgo8Iwgw=
 github.com/qor5/ui v1.0.1-0.20230913083355-743825ff29b1/go.mod h1:bgBqjIytHRdfTsiZea8df/ltAcyQyuHiLbecgo8Iwgw=
-github.com/qor5/web v1.2.5-0.20230905084205-145cb859a65f h1:xQwS/HKx9unrQ964UGkKiiXZPOpnLaQbRQ9DpaPJVKc=
-github.com/qor5/web v1.2.5-0.20230905084205-145cb859a65f/go.mod h1:4VXydGmy5Uwz8rEeKjcmCetciJo8TpU0mnN7Ca5kMR0=
+github.com/qor5/web v1.3.0 h1:nz/MP1TJ/ffGA6FgWXJ1ljPZDfd3RQSTHz/4ZrzKqX0=
+github.com/qor5/web v1.3.0/go.mod h1:4VXydGmy5Uwz8rEeKjcmCetciJo8TpU0mnN7Ca5kMR0=
 github.com/qor5/x v1.2.1-0.20230907054212-50b1a850acf6 h1:GyPeYULwjUPGR6fT/lZicJ8dkoKL5cu/hRNefxX+V7g=
 github.com/qor5/x v1.2.1-0.20230907054212-50b1a850acf6 h1:GyPeYULwjUPGR6fT/lZicJ8dkoKL5cu/hRNefxX+V7g=
 github.com/qor5/x v1.2.1-0.20230907054212-50b1a850acf6/go.mod h1:Zfy7B3X5DnQSud0HTV4h/ih5TTQgaT2NWwuSIRGLdcM=
 github.com/qor5/x v1.2.1-0.20230907054212-50b1a850acf6/go.mod h1:Zfy7B3X5DnQSud0HTV4h/ih5TTQgaT2NWwuSIRGLdcM=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=

+ 1 - 1
media/upload.go

@@ -77,7 +77,7 @@ func cropField(field *schema.Field, db *gorm.DB) (cropped bool, err error) {
 	// Save File
 	// Save File
 	if !handled {
 	if !handled {
 		err = media.Store(media.URL(), option, mediaFile)
 		err = media.Store(media.URL(), option, mediaFile)
-		return false, err
+		return true, err
 	}
 	}
 
 
 	return true, nil
 	return true, nil

+ 20 - 3
pagebuilder/builder.go

@@ -1464,7 +1464,24 @@ func (b *Builder) ConfigSharedContainer(pb *presets.Builder, db *gorm.DB) (pm *p
 	pm.RegisterEventFunc(republishRelatedOnlinePagesEvent, republishRelatedOnlinePages(b.mb.Info().ListingHref()))
 	pm.RegisterEventFunc(republishRelatedOnlinePagesEvent, republishRelatedOnlinePages(b.mb.Info().ListingHref()))
 
 
 	listing := pm.Listing("DisplayName").SearchColumns("display_name")
 	listing := pm.Listing("DisplayName").SearchColumns("display_name")
-	listing.RowMenu("").Empty()
+	listing.RowMenu("Rename").RowMenuItem("Rename").ComponentFunc(func(obj interface{}, id string, ctx *web.EventContext) h.HTMLComponent {
+		c := obj.(*Container)
+		msgr := i18n.MustGetModuleMessages(ctx.R, I18nPageBuilderKey, Messages_en_US).(*Messages)
+		return VListItem(
+			VListItemIcon(VIcon("edit_note")),
+
+			VListItemTitle(h.Text(msgr.Rename)),
+		).Attr("@click",
+			web.Plaid().
+				URL(b.ContainerByName(c.ModelName).mb.Info().ListingHref()).
+				EventFunc(RenameContainerDialogEvent).
+				Query(paramContainerID, c.PrimarySlug()).
+				Query(paramContainerName, c.DisplayName).
+				Query("portal", "presets").
+				Go(),
+		)
+	})
+
 	// ed := pm.Editing("SelectContainer")
 	// ed := pm.Editing("SelectContainer")
 	// ed.Field("SelectContainer").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
 	// ed.Field("SelectContainer").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
 	//	var containers []h.HTMLComponent
 	//	var containers []h.HTMLComponent
@@ -1728,7 +1745,7 @@ func sharedContainerSearcher(db *gorm.DB, mb *presets.ModelBuilder) presets.Sear
 
 
 		locale, _ := l10n.IsLocalizableFromCtx(ctx.R.Context())
 		locale, _ := l10n.IsLocalizableFromCtx(ctx.R.Context())
 		var c int64
 		var c int64
-		if err = wh.Select("count(display_name)").Where("shared = true AND locale_code = ?", locale).Group("display_name,model_name,model_id").Count(&c).Error; err != nil {
+		if err = wh.Select("count(display_name)").Where("shared = true AND locale_code = ?", locale).Group("display_name, model_name, model_id, locale_code").Count(&c).Error; err != nil {
 			return
 			return
 		}
 		}
 		totalCount = int(c)
 		totalCount = int(c)
@@ -1743,7 +1760,7 @@ func sharedContainerSearcher(db *gorm.DB, mb *presets.ModelBuilder) presets.Sear
 			wh = wh.Offset(int(offset))
 			wh = wh.Offset(int(offset))
 		}
 		}
 
 
-		if err = wh.Select("display_name,model_name,model_id").Find(obj).Error; err != nil {
+		if err = wh.Select("MIN(id) AS id, display_name, model_name, model_id, locale_code").Find(obj).Error; err != nil {
 			return
 			return
 		}
 		}
 		r = reflect.ValueOf(obj).Elem().Interface()
 		r = reflect.ValueOf(obj).Elem().Interface()

+ 28 - 19
pagebuilder/editor.go

@@ -563,7 +563,7 @@ func (b *Builder) ToggleContainerVisibility(ctx *web.EventContext) (r web.EventR
 	containerID := cs["id"]
 	containerID := cs["id"]
 	locale := cs["locale_code"]
 	locale := cs["locale_code"]
 
 
-	err = b.db.Exec("UPDATE page_builder_containers SET hidden = NOT(COALESCE(hidden,FALSE)) WHERE id = ? AND locale_code = ?", containerID, locale).Error
+	err = b.db.Exec("UPDATE page_builder_containers SET hidden = NOT(coalesce(hidden,FALSE)) WHERE id = ? AND locale_code = ?", containerID, locale).Error
 
 
 	r.PushState = web.Location(url.Values{})
 	r.PushState = web.Location(url.Values{})
 	return
 	return
@@ -758,8 +758,8 @@ func (b *Builder) localizeContainersToAnotherPage(db *gorm.DB, pageID int, pageV
 			newModelID = reflectutils.MustGet(model, "ID").(uint)
 			newModelID = reflectutils.MustGet(model, "ID").(uint)
 		} else {
 		} else {
 			var count int64
 			var count int64
-			var temp Container
-			if err = db.Where("model_name = ? AND locale_code = ?", c.ModelName, toPageLocale).First(&temp).Count(&count).Error; err != nil && err != gorm.ErrRecordNotFound {
+			var sharedCon Container
+			if err = db.Where("model_name = ? AND localize_from_model_id = ? AND locale_code = ? AND shared = ?", c.ModelName, c.ModelID, toPageLocale, true).First(&sharedCon).Count(&count).Error; err != nil && err != gorm.ErrRecordNotFound {
 				return
 				return
 			}
 			}
 
 
@@ -776,24 +776,29 @@ func (b *Builder) localizeContainersToAnotherPage(db *gorm.DB, pageID int, pageV
 				}
 				}
 				newModelID = reflectutils.MustGet(model, "ID").(uint)
 				newModelID = reflectutils.MustGet(model, "ID").(uint)
 			} else {
 			} else {
-				newModelID = temp.ModelID
-				newDisplayName = temp.DisplayName
+				newModelID = sharedCon.ModelID
+				newDisplayName = sharedCon.DisplayName
 			}
 			}
 		}
 		}
 
 
-		if err = db.Create(&Container{
-			Model:        gorm.Model{ID: c.ID},
-			PageID:       uint(toPageID),
-			PageVersion:  toPageVersion,
-			ModelName:    c.ModelName,
-			DisplayName:  newDisplayName,
-			ModelID:      newModelID,
-			DisplayOrder: c.DisplayOrder,
-			Shared:       c.Shared,
-			Locale: l10n.Locale{
-				LocaleCode: toPageLocale,
-			},
-		}).Error; err != nil {
+		var newCon Container
+		err = db.Order("display_order ASC").Find(&newCon, "id = ? AND locale_code = ?", c.ID, toPageLocale).Error
+		if err != nil {
+			return
+		}
+
+		newCon.ID = c.ID
+		newCon.PageID = uint(toPageID)
+		newCon.PageVersion = toPageVersion
+		newCon.ModelName = c.ModelName
+		newCon.DisplayName = newDisplayName
+		newCon.ModelID = newModelID
+		newCon.DisplayOrder = c.DisplayOrder
+		newCon.Shared = c.Shared
+		newCon.LocaleCode = toPageLocale
+		newCon.LocalizeFromModelID = c.ModelID
+
+		if err = db.Save(&newCon).Error; err != nil {
 			return
 			return
 		}
 		}
 	}
 	}
@@ -865,8 +870,12 @@ func (b *Builder) RenameContainerDialog(ctx *web.EventContext) (r web.EventRespo
 	okAction := web.Plaid().
 	okAction := web.Plaid().
 		URL(fmt.Sprintf("%s/editors", b.prefix)).
 		URL(fmt.Sprintf("%s/editors", b.prefix)).
 		EventFunc(RenameContainerEvent).Query(paramContainerID, paramID).Go()
 		EventFunc(RenameContainerEvent).Query(paramContainerID, paramID).Go()
+	portalName := dialogPortalName
+	if ctx.R.FormValue("portal") == "presets" {
+		portalName = presets.DialogPortalName
+	}
 	r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
 	r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
-		Name: dialogPortalName,
+		Name: portalName,
 		Body: web.Scope(
 		Body: web.Scope(
 			VDialog(
 			VDialog(
 				VCard(
 				VCard(

+ 4 - 0
pagebuilder/messages.go

@@ -25,6 +25,7 @@ type Messages struct {
 	FilterTabAllVersions           string
 	FilterTabAllVersions           string
 	FilterTabOnlineVersion         string
 	FilterTabOnlineVersion         string
 	FilterTabNamedVersions         string
 	FilterTabNamedVersions         string
+	Rename                         string
 }
 }
 
 
 var Messages_en_US = &Messages{
 var Messages_en_US = &Messages{
@@ -48,6 +49,7 @@ var Messages_en_US = &Messages{
 	FilterTabAllVersions:           "All Versions",
 	FilterTabAllVersions:           "All Versions",
 	FilterTabOnlineVersion:         "Online Version",
 	FilterTabOnlineVersion:         "Online Version",
 	FilterTabNamedVersions:         "Named Versions",
 	FilterTabNamedVersions:         "Named Versions",
+	Rename:                         "Rename",
 }
 }
 
 
 var Messages_zh_CN = &Messages{
 var Messages_zh_CN = &Messages{
@@ -71,6 +73,7 @@ var Messages_zh_CN = &Messages{
 	FilterTabAllVersions:           "所有版本",
 	FilterTabAllVersions:           "所有版本",
 	FilterTabOnlineVersion:         "在线版本",
 	FilterTabOnlineVersion:         "在线版本",
 	FilterTabNamedVersions:         "已命名版本",
 	FilterTabNamedVersions:         "已命名版本",
+	Rename:                         "重命名",
 }
 }
 
 
 var Messages_ja_JP = &Messages{
 var Messages_ja_JP = &Messages{
@@ -93,4 +96,5 @@ var Messages_ja_JP = &Messages{
 	FilterTabAllVersions:           "全てのバージョン",
 	FilterTabAllVersions:           "全てのバージョン",
 	FilterTabOnlineVersion:         "オンラインバージョン",
 	FilterTabOnlineVersion:         "オンラインバージョン",
 	FilterTabNamedVersions:         "名付け済みバージョン",
 	FilterTabNamedVersions:         "名付け済みバージョン",
+	Rename:                         "名前の変更",
 }
 }

+ 1 - 0
pagebuilder/models.go

@@ -141,6 +141,7 @@ type Container struct {
 	DisplayName  string
 	DisplayName  string
 
 
 	l10n.Locale
 	l10n.Locale
+	LocalizeFromModelID uint
 }
 }
 
 
 func (c *Container) PrimarySlug() string {
 func (c *Container) PrimarySlug() string {

+ 10 - 6
publish/views/versions.go

@@ -304,19 +304,23 @@ func duplicateVersionAction(db *gorm.DB, mb *presets.ModelBuilder, publisher *pu
 		currentVersionName := slugger.PrimaryColumnValuesBySlug(ctx.R.FormValue(presets.ParamID))["version"]
 		currentVersionName := slugger.PrimaryColumnValuesBySlug(ctx.R.FormValue(presets.ParamID))["version"]
 		paramID := ctx.R.FormValue(presets.ParamID)
 		paramID := ctx.R.FormValue(presets.ParamID)
 		me := mb.Editing()
 		me := mb.Editing()
-		vErr := me.RunSetterFunc(ctx, false, toObj)
-		if vErr.HaveErrors() {
-			presets.ShowMessage(&r, vErr.Error(), "error")
-			return
-		}
 
 
 		var fromObj = mb.NewModel()
 		var fromObj = mb.NewModel()
-		utils.PrimarySluggerWhere(db, mb.NewModel(), paramID).First(fromObj)
+		if err = utils.PrimarySluggerWhere(db, mb.NewModel(), paramID).First(fromObj).Error; err != nil {
+			return
+		}
 		if err = utils.SetPrimaryKeys(fromObj, toObj, db, paramID); err != nil {
 		if err = utils.SetPrimaryKeys(fromObj, toObj, db, paramID); err != nil {
 			presets.ShowMessage(&r, err.Error(), "error")
 			presets.ShowMessage(&r, err.Error(), "error")
 			return
 			return
 		}
 		}
 
 
+		if vErr := me.SetObjectFields(fromObj, toObj, &presets.FieldContext{
+			ModelInfo: mb.Info(),
+		}, false, presets.ContextModifiedIndexesBuilder(ctx).FromHidden(ctx.R), ctx); vErr.HaveErrors() {
+			presets.ShowMessage(&r, vErr.Error(), "error")
+			return
+		}
+
 		if err = reflectutils.Set(toObj, "Version.ParentVersion", currentVersionName); err != nil {
 		if err = reflectutils.Set(toObj, "Version.ParentVersion", currentVersionName); err != nil {
 			presets.ShowMessage(&r, err.Error(), "error")
 			presets.ShowMessage(&r, err.Error(), "error")
 			return
 			return

+ 67 - 20
seo/admin.go

@@ -58,9 +58,32 @@ func (collection *Collection) Configure(b *presets.Builder, db *gorm.DB) {
 
 
 func EditSetterFunc(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) (err error) {
 func EditSetterFunc(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) (err error) {
 	var setting Setting
 	var setting Setting
-	for key := range ctx.R.Form {
-		if strings.HasPrefix(key, fmt.Sprintf("%s.", field.Name)) {
-			reflectutils.Set(&setting, strings.TrimPrefix(key, fmt.Sprintf("%s.", field.Name)), ctx.R.Form.Get(key))
+	var mediaBox = media_library.MediaBox{}
+	for fieldWithPrefix := range ctx.R.Form {
+		// make sure OpenGraphImageFromMediaLibrary.Description set after OpenGraphImageFromMediaLibrary.Values
+		if fieldWithPrefix == fmt.Sprintf("%s.%s", field.Name, "OpenGraphImageFromMediaLibrary.Values") {
+			err = mediaBox.Scan(ctx.R.FormValue(fieldWithPrefix))
+			if err != nil {
+				return
+			}
+			break
+		}
+	}
+	for fieldWithPrefix := range ctx.R.Form {
+		if strings.HasPrefix(fieldWithPrefix, fmt.Sprintf("%s.%s", field.Name, "OpenGraphImageFromMediaLibrary")) {
+			if fieldWithPrefix == fmt.Sprintf("%s.%s", field.Name, "OpenGraphImageFromMediaLibrary.Description") {
+				mediaBox.Description = ctx.R.Form.Get(fieldWithPrefix)
+				reflectutils.Set(&setting, "OpenGraphImageFromMediaLibrary", mediaBox)
+			}
+			continue
+		}
+		if fieldWithPrefix == fmt.Sprintf("%s.%s", field.Name, "OpenGraphMetadataString") {
+			metadata := GetOpenGraphMetadata(ctx.R.Form.Get(fieldWithPrefix))
+			reflectutils.Set(&setting, "OpenGraphMetadata", metadata)
+			continue
+		}
+		if strings.HasPrefix(fieldWithPrefix, fmt.Sprintf("%s.", field.Name)) {
+			reflectutils.Set(&setting, strings.TrimPrefix(fieldWithPrefix, fmt.Sprintf("%s.", field.Name)), ctx.R.Form.Get(fieldWithPrefix))
 		}
 		}
 	}
 	}
 	return reflectutils.Set(obj, field.Name, setting)
 	return reflectutils.Set(obj, field.Name, setting)
@@ -93,6 +116,8 @@ func (collection *Collection) EditingComponentFunc(obj interface{}, field *prese
 		setting.Title = modelSetting.GetTitle()
 		setting.Title = modelSetting.GetTitle()
 		setting.Description = modelSetting.GetDescription()
 		setting.Description = modelSetting.GetDescription()
 		setting.Keywords = modelSetting.GetKeywords()
 		setting.Keywords = modelSetting.GetKeywords()
+		setting.OpenGraphTitle = modelSetting.GetOpenGraphTitle()
+		setting.OpenGraphDescription = modelSetting.GetOpenGraphDescription()
 		setting.OpenGraphURL = modelSetting.GetOpenGraphURL()
 		setting.OpenGraphURL = modelSetting.GetOpenGraphURL()
 		setting.OpenGraphType = modelSetting.GetOpenGraphType()
 		setting.OpenGraphType = modelSetting.GetOpenGraphType()
 		setting.OpenGraphImageURL = modelSetting.GetOpenGraphImageURL()
 		setting.OpenGraphImageURL = modelSetting.GetOpenGraphImageURL()
@@ -207,7 +232,6 @@ func (collection *Collection) pageFunc(ctx *web.EventContext) (_ web.PageRespons
 		)
 		)
 		seoComponents = append(seoComponents, comp)
 		seoComponents = append(seoComponents, comp)
 	}
 	}
-
 	return web.PageResponse{
 	return web.PageResponse{
 		PageTitle: msgr.PageTitle,
 		PageTitle: msgr.PageTitle,
 		Body: h.If(editIsAllowed(ctx.R) == nil, VContainer(
 		Body: h.If(editIsAllowed(ctx.R) == nil, VContainer(
@@ -238,7 +262,6 @@ func (collection *Collection) vseo(fieldPrefix string, seo *SEO, setting *Settin
 		msgr = i18n.MustGetModuleMessages(req, I18nSeoKey, Messages_en_US).(*Messages)
 		msgr = i18n.MustGetModuleMessages(req, I18nSeoKey, Messages_en_US).(*Messages)
 		db   = collection.getDBFromContext(req.Context())
 		db   = collection.getDBFromContext(req.Context())
 	)
 	)
-
 	if seo.name == collection.globalName {
 	if seo.name == collection.globalName {
 		seos = append(seos, seo)
 		seos = append(seos, seo)
 	} else {
 	} else {
@@ -284,9 +307,9 @@ func (collection *Collection) vseo(fieldPrefix string, seo *SEO, setting *Settin
 		),
 		),
 		VCard(
 		VCard(
 			VCardText(
 			VCardText(
-				VTextField().Counter(65).FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "Title")).Label(msgr.Title).Value(setting.Title).Attr("@click", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_title", refPrefix))).Attr("ref", fmt.Sprintf("%s_title", refPrefix)),
-				VTextField().Counter(150).FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "Description")).Label(msgr.Description).Value(setting.Description).Attr("@click", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_description", refPrefix))).Attr("ref", fmt.Sprintf("%s_description", refPrefix)),
-				VTextarea().Counter(255).Rows(2).AutoGrow(true).FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "Keywords")).Label(msgr.Keywords).Value(setting.Keywords).Attr("@click", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_keywords", refPrefix))).Attr("ref", fmt.Sprintf("%s_keywords", refPrefix)),
+				VTextField().Counter(65).FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "Title")).Label(msgr.Title).Value(setting.Title).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_title", refPrefix))).Attr("ref", fmt.Sprintf("%s_title", refPrefix)),
+				VTextField().Counter(150).FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "Description")).Label(msgr.Description).Value(setting.Description).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_description", refPrefix))).Attr("ref", fmt.Sprintf("%s_description", refPrefix)),
+				VTextarea().Counter(255).Rows(2).AutoGrow(true).FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "Keywords")).Label(msgr.Keywords).Value(setting.Keywords).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_keywords", refPrefix))).Attr("ref", fmt.Sprintf("%s_keywords", refPrefix)),
 			),
 			),
 		).Outlined(true).Flat(true),
 		).Outlined(true).Flat(true),
 
 
@@ -294,11 +317,15 @@ func (collection *Collection) vseo(fieldPrefix string, seo *SEO, setting *Settin
 		VCard(
 		VCard(
 			VCardText(
 			VCardText(
 				VRow(
 				VRow(
-					VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphURL")).Label(msgr.OpenGraphURL).Value(setting.OpenGraphURL)).Cols(6),
-					VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphType")).Label(msgr.OpenGraphType).Value(setting.OpenGraphType)).Cols(6),
+					VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphTitle")).Label(msgr.OpenGraphTitle).Value(setting.OpenGraphTitle).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_og_title", refPrefix))).Attr("ref", fmt.Sprintf("%s_og_title", refPrefix))).Cols(6),
+					VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphDescription")).Label(msgr.OpenGraphDescription).Value(setting.OpenGraphDescription).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_og_description", refPrefix))).Attr("ref", fmt.Sprintf("%s_og_description", refPrefix))).Cols(6),
+				),
+				VRow(
+					VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphURL")).Label(msgr.OpenGraphURL).Value(setting.OpenGraphURL).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_og_url", refPrefix))).Attr("ref", fmt.Sprintf("%s_og_url", refPrefix))).Cols(6),
+					VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphType")).Label(msgr.OpenGraphType).Value(setting.OpenGraphType).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_og_type", refPrefix))).Attr("ref", fmt.Sprintf("%s_og_type", refPrefix))).Cols(6),
 				),
 				),
 				VRow(
 				VRow(
-					VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphImageURL")).Label(msgr.OpenGraphImageURL).Value(setting.OpenGraphImageURL)).Cols(12),
+					VCol(VTextField().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphImageURL")).Label(msgr.OpenGraphImageURL).Value(setting.OpenGraphImageURL).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_og_imageurl", refPrefix))).Attr("ref", fmt.Sprintf("%s_og_imageurl", refPrefix))).Cols(12),
 				),
 				),
 				VRow(
 				VRow(
 					VCol(views.QMediaBox(db).Label(msgr.OpenGraphImage).
 					VCol(views.QMediaBox(db).Label(msgr.OpenGraphImage).
@@ -321,6 +348,9 @@ func (collection *Collection) vseo(fieldPrefix string, seo *SEO, setting *Settin
 								},
 								},
 							},
 							},
 						})).Cols(12)),
 						})).Cols(12)),
+				VRow(
+					VCol(VTextarea().FieldName(fmt.Sprintf("%s.%s", fieldPrefix, "OpenGraphMetadataString")).Label(msgr.OpenGraphMetadata).Value(GetOpenGraphMetadataString(setting.OpenGraphMetadata)).Attr("@focus", fmt.Sprintf("$refs.seo.tagInputsFocus($refs.%s)", fmt.Sprintf("%s_og_metadata", refPrefix))).Attr("ref", fmt.Sprintf("%s_og_metadata", refPrefix))).Cols(12),
+				),
 			),
 			),
 		).Outlined(true).Flat(true),
 		).Outlined(true).Flat(true),
 	).Attr("ref", "seo")
 	).Attr("ref", "seo")
@@ -348,24 +378,33 @@ func (collection *Collection) save(ctx *web.EventContext) (r web.EventResponse,
 		if !strings.HasPrefix(fieldWithPrefix, name) {
 		if !strings.HasPrefix(fieldWithPrefix, name) {
 			continue
 			continue
 		}
 		}
-
 		field := strings.Replace(fieldWithPrefix, fmt.Sprintf("%s.", name), "", -1)
 		field := strings.Replace(fieldWithPrefix, fmt.Sprintf("%s.", name), "", -1)
-		if strings.HasPrefix(field, "OpenGraphImageFromMediaLibrary") {
-			if field == "OpenGraphImageFromMediaLibrary.Values" {
-				err = mediaBox.Scan(ctx.R.FormValue(fieldWithPrefix))
-				if err != nil {
-					return
-				}
-				settingVals["OpenGraphImageFromMediaLibrary"] = mediaBox
+		// make sure OpenGraphImageFromMediaLibrary.Description set after OpenGraphImageFromMediaLibrary.Values
+		if field == "OpenGraphImageFromMediaLibrary.Values" {
+			err = mediaBox.Scan(ctx.R.FormValue(fieldWithPrefix))
+			if err != nil {
+				return
 			}
 			}
+			break
+		}
+	}
 
 
+	for fieldWithPrefix := range ctx.R.Form {
+		if !strings.HasPrefix(fieldWithPrefix, name) {
+			continue
+		}
+		field := strings.Replace(fieldWithPrefix, fmt.Sprintf("%s.", name), "", -1)
+		if strings.HasPrefix(field, "OpenGraphImageFromMediaLibrary") {
 			if field == "OpenGraphImageFromMediaLibrary.Description" {
 			if field == "OpenGraphImageFromMediaLibrary.Description" {
 				mediaBox.Description = ctx.R.FormValue(fieldWithPrefix)
 				mediaBox.Description = ctx.R.FormValue(fieldWithPrefix)
 				if err != nil {
 				if err != nil {
 					return
 					return
 				}
 				}
+				settingVals["OpenGraphImageFromMediaLibrary"] = mediaBox
 			}
 			}
-		} else if strings.HasPrefix(field, "Variables") {
+			continue
+		}
+		if strings.HasPrefix(field, "Variables") {
 			key := strings.Replace(field, "Variables.", "", -1)
 			key := strings.Replace(field, "Variables.", "", -1)
 			variables[key] = ctx.R.FormValue(fieldWithPrefix)
 			variables[key] = ctx.R.FormValue(fieldWithPrefix)
 		} else {
 		} else {
@@ -374,6 +413,14 @@ func (collection *Collection) save(ctx *web.EventContext) (r web.EventResponse,
 	}
 	}
 	s := setting.GetSEOSetting()
 	s := setting.GetSEOSetting()
 	for k, v := range settingVals {
 	for k, v := range settingVals {
+		if k == "OpenGraphMetadataString" {
+			metadata := GetOpenGraphMetadata(v.(string))
+			err = reflectutils.Set(&s, "OpenGraphMetadata", metadata)
+			if err != nil {
+				return
+			}
+			continue
+		}
 		err = reflectutils.Set(&s, k, v)
 		err = reflectutils.Set(&s, k, v)
 		if err != nil {
 		if err != nil {
 			return
 			return

+ 19 - 0
seo/collection.go

@@ -296,6 +296,12 @@ func (collection Collection) Render(obj interface{}, req *http.Request) h.HTMLCo
 		if s.Keywords != "" && setting.Keywords == "" {
 		if s.Keywords != "" && setting.Keywords == "" {
 			setting.Keywords = s.Keywords
 			setting.Keywords = s.Keywords
 		}
 		}
+		if s.OpenGraphTitle != "" && setting.OpenGraphTitle == "" {
+			setting.OpenGraphTitle = s.OpenGraphTitle
+		}
+		if s.OpenGraphDescription != "" && setting.OpenGraphDescription == "" {
+			setting.OpenGraphDescription = s.OpenGraphDescription
+		}
 		if s.OpenGraphURL != "" && setting.OpenGraphURL == "" {
 		if s.OpenGraphURL != "" && setting.OpenGraphURL == "" {
 			setting.OpenGraphURL = s.OpenGraphURL
 			setting.OpenGraphURL = s.OpenGraphURL
 		}
 		}
@@ -374,6 +380,19 @@ func replaceVariables(setting Setting, values map[string]string) Setting {
 	setting.Title = replace(setting.Title)
 	setting.Title = replace(setting.Title)
 	setting.Description = replace(setting.Description)
 	setting.Description = replace(setting.Description)
 	setting.Keywords = replace(setting.Keywords)
 	setting.Keywords = replace(setting.Keywords)
+	setting.OpenGraphTitle = replace(setting.OpenGraphTitle)
+	setting.OpenGraphDescription = replace(setting.OpenGraphDescription)
+	setting.OpenGraphURL = replace(setting.OpenGraphURL)
+	setting.OpenGraphType = replace(setting.OpenGraphType)
+	setting.OpenGraphImageURL = replace(setting.OpenGraphImageURL)
+	var metadata []OpenGraphMetadata
+	for _, m := range setting.OpenGraphMetadata {
+		metadata = append(metadata, OpenGraphMetadata{
+			Property: m.Property,
+			Content:  replace(m.Content),
+		})
+	}
+	setting.OpenGraphMetadata = metadata
 	return setting
 	return setting
 }
 }
 
 

+ 9 - 0
seo/messages.go

@@ -11,10 +11,13 @@ type Messages struct {
 	Description             string
 	Description             string
 	Keywords                string
 	Keywords                string
 	OpenGraphInformation    string
 	OpenGraphInformation    string
+	OpenGraphTitle          string
+	OpenGraphDescription    string
 	OpenGraphURL            string
 	OpenGraphURL            string
 	OpenGraphType           string
 	OpenGraphType           string
 	OpenGraphImageURL       string
 	OpenGraphImageURL       string
 	OpenGraphImage          string
 	OpenGraphImage          string
+	OpenGraphMetadata       string
 	Save                    string
 	Save                    string
 	SavedSuccessfully       string
 	SavedSuccessfully       string
 	Seo                     string
 	Seo                     string
@@ -32,10 +35,13 @@ var Messages_en_US = &Messages{
 	Description:             "Description",
 	Description:             "Description",
 	Keywords:                "Keywords",
 	Keywords:                "Keywords",
 	OpenGraphInformation:    "Open Graph Information",
 	OpenGraphInformation:    "Open Graph Information",
+	OpenGraphTitle:          "Open Graph Title",
+	OpenGraphDescription:    "Open Graph Description",
 	OpenGraphURL:            "Open Graph URL",
 	OpenGraphURL:            "Open Graph URL",
 	OpenGraphType:           "Open Graph Type",
 	OpenGraphType:           "Open Graph Type",
 	OpenGraphImageURL:       "Open Graph Image URL",
 	OpenGraphImageURL:       "Open Graph Image URL",
 	OpenGraphImage:          "Open Graph Image",
 	OpenGraphImage:          "Open Graph Image",
+	OpenGraphMetadata:       "Open Graph Metadata",
 	Save:                    "Save",
 	Save:                    "Save",
 	SavedSuccessfully:       "Saved successfully",
 	SavedSuccessfully:       "Saved successfully",
 	Seo:                     "SEO",
 	Seo:                     "SEO",
@@ -53,10 +59,13 @@ var Messages_zh_CN = &Messages{
 	Description:             "描述",
 	Description:             "描述",
 	Keywords:                "关键词",
 	Keywords:                "关键词",
 	OpenGraphInformation:    "OG 信息",
 	OpenGraphInformation:    "OG 信息",
+	OpenGraphTitle:          "OG 标题",
+	OpenGraphDescription:    "OG 描述",
 	OpenGraphURL:            "OG 链接",
 	OpenGraphURL:            "OG 链接",
 	OpenGraphType:           "OG 类型",
 	OpenGraphType:           "OG 类型",
 	OpenGraphImageURL:       "OG 图片链接",
 	OpenGraphImageURL:       "OG 图片链接",
 	OpenGraphImage:          "OG 图片",
 	OpenGraphImage:          "OG 图片",
+	OpenGraphMetadata:       "OG 元数据",
 	Save:                    "保存",
 	Save:                    "保存",
 	SavedSuccessfully:       "成功保存",
 	SavedSuccessfully:       "成功保存",
 	Seo:                     "搜索引擎优化",
 	Seo:                     "搜索引擎优化",

+ 49 - 4
seo/model.go

@@ -1,8 +1,11 @@
 package seo
 package seo
 
 
 import (
 import (
+	"bytes"
 	"database/sql/driver"
 	"database/sql/driver"
+	"encoding/csv"
 	"encoding/json"
 	"encoding/json"
+	"strings"
 	"time"
 	"time"
 
 
 	"github.com/qor5/admin/l10n"
 	"github.com/qor5/admin/l10n"
@@ -24,6 +27,8 @@ type QorSEOSettingInterface interface {
 	GetTitle() string
 	GetTitle() string
 	GetDescription() string
 	GetDescription() string
 	GetKeywords() string
 	GetKeywords() string
+	GetOpenGraphTitle() string
+	GetOpenGraphDescription() string
 	GetOpenGraphURL() string
 	GetOpenGraphURL() string
 	GetOpenGraphType() string
 	GetOpenGraphType() string
 	GetOpenGraphImageURL() string
 	GetOpenGraphImageURL() string
@@ -49,6 +54,8 @@ type Setting struct {
 	Title                          string `gorm:"size:4294967295"`
 	Title                          string `gorm:"size:4294967295"`
 	Description                    string
 	Description                    string
 	Keywords                       string
 	Keywords                       string
+	OpenGraphTitle                 string
+	OpenGraphDescription           string
 	OpenGraphURL                   string
 	OpenGraphURL                   string
 	OpenGraphType                  string
 	OpenGraphType                  string
 	OpenGraphImageURL              string
 	OpenGraphImageURL              string
@@ -102,6 +109,12 @@ func (s *QorSEOSetting) SetName(name string) {
 	s.Name = name
 	s.Name = name
 }
 }
 
 
+func (s QorSEOSetting) GetOpenGraphTitle() string {
+	return s.Setting.OpenGraphTitle
+}
+func (s QorSEOSetting) GetOpenGraphDescription() string {
+	return s.Setting.OpenGraphDescription
+}
 func (s QorSEOSetting) GetOpenGraphURL() string {
 func (s QorSEOSetting) GetOpenGraphURL() string {
 	return s.Setting.OpenGraphURL
 	return s.Setting.OpenGraphURL
 }
 }
@@ -154,7 +167,10 @@ func (setting Setting) Value() (driver.Value, error) {
 }
 }
 
 
 func (setting Setting) IsEmpty() bool {
 func (setting Setting) IsEmpty() bool {
-	return setting.Title == "" && setting.Description == "" && setting.Keywords == "" && setting.OpenGraphURL == "" && setting.OpenGraphType == "" && setting.OpenGraphImageURL == "" && setting.OpenGraphImageFromMediaLibrary.Url == "" && len(setting.OpenGraphMetadata) == 0
+	return setting.Title == "" && setting.Description == "" && setting.Keywords == "" &&
+		setting.OpenGraphTitle == "" && setting.OpenGraphDescription == "" &&
+		setting.OpenGraphURL == "" && setting.OpenGraphType == "" && setting.OpenGraphImageURL == "" &&
+		setting.OpenGraphImageFromMediaLibrary.Url == "" && len(setting.OpenGraphMetadata) == 0
 }
 }
 
 
 type Variables map[string]string
 type Variables map[string]string
@@ -181,8 +197,8 @@ func (setting Variables) Value() (driver.Value, error) {
 
 
 func (setting Setting) HTMLComponent(tags map[string]string) h.HTMLComponent {
 func (setting Setting) HTMLComponent(tags map[string]string) h.HTMLComponent {
 	openGraphData := map[string]string{
 	openGraphData := map[string]string{
-		"og:title":       setting.Title,
-		"og:description": setting.Description,
+		"og:title":       setting.OpenGraphTitle,
+		"og:description": setting.OpenGraphDescription,
 		"og:url":         setting.OpenGraphURL,
 		"og:url":         setting.OpenGraphURL,
 		"og:type":        setting.OpenGraphType,
 		"og:type":        setting.OpenGraphType,
 		"og:image":       setting.OpenGraphImageURL,
 		"og:image":       setting.OpenGraphImageURL,
@@ -192,7 +208,7 @@ func (setting Setting) HTMLComponent(tags map[string]string) h.HTMLComponent {
 		openGraphData[metavalue.Property] = metavalue.Content
 		openGraphData[metavalue.Property] = metavalue.Content
 	}
 	}
 
 
-	for _, key := range []string{"og:url", "og:type", "og:image", "og:title", "og:description"} {
+	for _, key := range []string{"og:title", "og:description", "og:url", "og:type", "og:image"} {
 		if v := openGraphData[key]; v == "" {
 		if v := openGraphData[key]; v == "" {
 			if v, ok := tags[key]; ok {
 			if v, ok := tags[key]; ok {
 				openGraphData[key] = v
 				openGraphData[key] = v
@@ -222,3 +238,32 @@ func (setting Setting) HTMLComponent(tags map[string]string) h.HTMLComponent {
 		openGraphDataComponents,
 		openGraphDataComponents,
 	}
 	}
 }
 }
+
+func GetOpenGraphMetadata(in string) (metadata []OpenGraphMetadata) {
+	r := csv.NewReader(strings.NewReader(in))
+	records, err := r.ReadAll()
+	if err != nil {
+		return
+	}
+	for _, row := range records {
+		if len(row) != 2 {
+			continue
+		}
+		metadata = append(metadata, OpenGraphMetadata{
+			Property: row[0],
+			Content:  row[1],
+		})
+	}
+	return
+}
+
+func GetOpenGraphMetadataString(metadata []OpenGraphMetadata) string {
+	records := [][]string{}
+	for _, m := range metadata {
+		records = append(records, []string{m.Property, m.Content})
+	}
+	buf := new(bytes.Buffer)
+	w := csv.NewWriter(buf)
+	w.WriteAll(records)
+	return buf.String()
+}

+ 20 - 16
seo/model_test.go

@@ -15,34 +15,38 @@ func TestSettingHTMLComponent(t *testing.T) {
 		{
 		{
 			name: "Render the seo html",
 			name: "Render the seo html",
 			setting: Setting{
 			setting: Setting{
-				Title:             "title",
-				Description:       "description",
-				Keywords:          "keyword",
-				OpenGraphURL:      "http://dev.qor5.com/product/1",
-				OpenGraphType:     "",
-				OpenGraphImageURL: "http://dev.qor5.com/product/1/og.jpg",
+				Title:                "title",
+				Description:          "description",
+				Keywords:             "keyword",
+				OpenGraphTitle:       "og title",
+				OpenGraphDescription: "og description",
+				OpenGraphURL:         "http://dev.qor5.com/product/1",
+				OpenGraphType:        "",
+				OpenGraphImageURL:    "http://dev.qor5.com/product/1/og.jpg",
 			},
 			},
 			tags: map[string]string{},
 			tags: map[string]string{},
 			want: `
 			want: `
 			<title>title</title>
 			<title>title</title>
 			<meta name='description' content='description'>
 			<meta name='description' content='description'>
 			<meta name='keywords' content='keyword'>
 			<meta name='keywords' content='keyword'>
+			<meta property='og:title' name='og:title' content='og title'>
+			<meta property='og:description' name='og:description' content='og description'>
 			<meta property='og:type' name='og:type' content='website'>
 			<meta property='og:type' name='og:type' content='website'>
 			<meta property='og:image' name='og:image' content='http://dev.qor5.com/product/1/og.jpg'>
 			<meta property='og:image' name='og:image' content='http://dev.qor5.com/product/1/og.jpg'>
-			<meta property='og:title' name='og:title' content='title'>
-			<meta property='og:description' name='og:description' content='description'>
 			<meta property='og:url' name='og:url' content='http://dev.qor5.com/product/1'>`,
 			<meta property='og:url' name='og:url' content='http://dev.qor5.com/product/1'>`,
 		},
 		},
 
 
 		{
 		{
 			name: "Render the seo html using the tag data",
 			name: "Render the seo html using the tag data",
 			setting: Setting{
 			setting: Setting{
-				Title:             "title",
-				Description:       "description",
-				Keywords:          "keyword",
-				OpenGraphURL:      "http://dev.qor5.com/product/1",
-				OpenGraphType:     "",
-				OpenGraphImageURL: "http://dev.qor5.com/product/1/og.jpg",
+				Title:                "title",
+				Description:          "description",
+				Keywords:             "keyword",
+				OpenGraphTitle:       "og title",
+				OpenGraphDescription: "og description",
+				OpenGraphURL:         "http://dev.qor5.com/product/1",
+				OpenGraphType:        "",
+				OpenGraphImageURL:    "http://dev.qor5.com/product/1/og.jpg",
 			},
 			},
 			tags: map[string]string{
 			tags: map[string]string{
 				"og:type":       "product",
 				"og:type":       "product",
@@ -52,10 +56,10 @@ func TestSettingHTMLComponent(t *testing.T) {
 			<title>title</title>
 			<title>title</title>
 			<meta name='description' content='description'>
 			<meta name='description' content='description'>
 			<meta name='keywords' content='keyword'>
 			<meta name='keywords' content='keyword'>
+			<meta property='og:title' name='og:title' content='og title'>
+			<meta property='og:description' name='og:description' content='og description'>
 			<meta property='og:type' name='og:type' content='product'>
 			<meta property='og:type' name='og:type' content='product'>
 			<meta property='og:image' name='og:image' content='http://dev.qor5.com/product/1/og.jpg'>
 			<meta property='og:image' name='og:image' content='http://dev.qor5.com/product/1/og.jpg'>
-			<meta property='og:title' name='og:title' content='title'>
-			<meta property='og:description' name='og:description' content='description'>
 			<meta property='og:url' name='og:url' content='http://dev.qor5.com/product/1'>
 			<meta property='og:url' name='og:url' content='http://dev.qor5.com/product/1'>
 			<meta property='twiiter:image' name='twiiter:image' content='http://dev.qor5.com/product/1/twitter.jpg'>`,
 			<meta property='twiiter:image' name='twiiter:image' content='http://dev.qor5.com/product/1/twitter.jpg'>`,
 		},
 		},

+ 2 - 2
worker/action_job.go

@@ -253,9 +253,9 @@ func (b *Builder) eventActionJobClose(ctx *web.EventContext) (er web.EventRespon
 
 
 	switch inst.Status {
 	switch inst.Status {
 	case JobStatusRunning:
 	case JobStatusRunning:
-		err = b.q.Kill(inst)
+		err = b.q.Kill(ctx.R.Context(), inst)
 	case JobStatusNew, JobStatusScheduled:
 	case JobStatusNew, JobStatusScheduled:
-		err = b.q.Remove(inst)
+		err = b.q.Remove(ctx.R.Context(), inst)
 	}
 	}
 
 
 	return er, err
 	return er, err

+ 13 - 8
worker/builder.go

@@ -1,6 +1,7 @@
 package worker
 package worker
 
 
 import (
 import (
+	"context"
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
@@ -392,6 +393,10 @@ func (b *Builder) Listen() {
 	}
 	}
 }
 }
 
 
+func (b *Builder) Shutdown(ctx context.Context) error {
+	return b.q.Shutdown(ctx)
+}
+
 func (b *Builder) createJob(ctx *web.EventContext, qorJob *QorJob) (j *QorJob, err error) {
 func (b *Builder) createJob(ctx *web.EventContext, qorJob *QorJob) (j *QorJob, err error) {
 	if err = editIsAllowed(ctx.R, qorJob.Job); err != nil {
 	if err = editIsAllowed(ctx.R, qorJob.Job); err != nil {
 		return
 		return
@@ -439,7 +444,7 @@ func (b *Builder) createJob(ctx *web.EventContext, qorJob *QorJob) (j *QorJob, e
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
-		return b.q.Add(inst)
+		return b.q.Add(ctx.R.Context(), inst)
 	})
 	})
 	return
 	return
 }
 }
@@ -477,7 +482,7 @@ func (b *Builder) eventAbortJob(ctx *web.EventContext) (er web.EventResponse, er
 	}
 	}
 	isScheduled := inst.Status == JobStatusScheduled
 	isScheduled := inst.Status == JobStatusScheduled
 
 
-	err = b.doAbortJob(inst)
+	err = b.doAbortJob(ctx.R.Context(), inst)
 	if err != nil {
 	if err != nil {
 		_, ok := err.(*cannotAbortError)
 		_, ok := err.(*cannotAbortError)
 		if !ok {
 		if !ok {
@@ -517,12 +522,12 @@ func (e *cannotAbortError) Error() string {
 	return e.err.Error()
 	return e.err.Error()
 }
 }
 
 
-func (b *Builder) doAbortJob(inst *QorJobInstance) (err error) {
+func (b *Builder) doAbortJob(ctx context.Context, inst *QorJobInstance) (err error) {
 	switch inst.Status {
 	switch inst.Status {
 	case JobStatusRunning:
 	case JobStatusRunning:
-		return b.q.Kill(inst)
+		return b.q.Kill(ctx, inst)
 	case JobStatusNew, JobStatusScheduled:
 	case JobStatusNew, JobStatusScheduled:
-		return b.q.Remove(inst)
+		return b.q.Remove(ctx, inst)
 	default:
 	default:
 		return &cannotAbortError{
 		return &cannotAbortError{
 			err: fmt.Errorf("job status is %s, cannot be aborted/canceled", inst.Status),
 			err: fmt.Errorf("job status is %s, cannot be aborted/canceled", inst.Status),
@@ -555,7 +560,7 @@ func (b *Builder) eventRerunJob(ctx *web.EventContext) (er web.EventResponse, er
 	if err != nil {
 	if err != nil {
 		return er, err
 		return er, err
 	}
 	}
-	err = b.q.Add(inst)
+	err = b.q.Add(ctx.R.Context(), inst)
 	if err != nil {
 	if err != nil {
 		return er, err
 		return er, err
 	}
 	}
@@ -604,7 +609,7 @@ func (b *Builder) eventUpdateJob(ctx *web.EventContext) (er web.EventResponse, e
 		return er, err
 		return er, err
 	}
 	}
 	oldArgs, _ := jb.parseArgs(old.Args)
 	oldArgs, _ := jb.parseArgs(old.Args)
-	err = b.doAbortJob(old)
+	err = b.doAbortJob(ctx.R.Context(), old)
 	if err != nil {
 	if err != nil {
 		_, ok := err.(*cannotAbortError)
 		_, ok := err.(*cannotAbortError)
 		if !ok {
 		if !ok {
@@ -624,7 +629,7 @@ func (b *Builder) eventUpdateJob(ctx *web.EventContext) (er web.EventResponse, e
 	if err != nil {
 	if err != nil {
 		return er, err
 		return er, err
 	}
 	}
-	err = b.q.Add(newInst)
+	err = b.q.Add(ctx.R.Context(), newInst)
 	if err != nil {
 	if err != nil {
 		return er, err
 		return er, err
 	}
 	}

+ 12 - 8
worker/cron.go

@@ -94,7 +94,7 @@ func (c *cron) writeCronJob() error {
 }
 }
 
 
 // Add a job to cron queue
 // Add a job to cron queue
-func (c *cron) Add(job QueJobInterface) (err error) {
+func (c *cron) Add(ctx context.Context, job QueJobInterface) (err error) {
 	c.parseJobs()
 	c.parseJobs()
 	defer c.writeCronJob()
 	defer c.writeCronJob()
 
 
@@ -135,7 +135,7 @@ func (c *cron) Add(job QueJobInterface) (err error) {
 }
 }
 
 
 // Run a job from cron queue
 // Run a job from cron queue
-func (c *cron) run(qorJob QueJobInterface) (err error) {
+func (c *cron) run(ctx context.Context, qorJob QueJobInterface) (err error) {
 	jobInfo, err := qorJob.GetJobInfo()
 	jobInfo, err := qorJob.GetJobInfo()
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -166,7 +166,7 @@ func (c *cron) run(qorJob QueJobInterface) (err error) {
 	qorJob.StartRefresh()
 	qorJob.StartRefresh()
 	defer qorJob.StopRefresh()
 	defer qorJob.StopRefresh()
 
 
-	err = h(context.Background(), qorJob)
+	err = h(ctx, qorJob)
 	if err == nil {
 	if err == nil {
 		c.parseJobs()
 		c.parseJobs()
 		defer c.writeCronJob()
 		defer c.writeCronJob()
@@ -180,7 +180,7 @@ func (c *cron) run(qorJob QueJobInterface) (err error) {
 }
 }
 
 
 // Kill a job from cron queue
 // Kill a job from cron queue
-func (c *cron) Kill(job QueJobInterface) (err error) {
+func (c *cron) Kill(ctx context.Context, job QueJobInterface) (err error) {
 	c.parseJobs()
 	c.parseJobs()
 	defer c.writeCronJob()
 	defer c.writeCronJob()
 
 
@@ -204,7 +204,7 @@ func (c *cron) Kill(job QueJobInterface) (err error) {
 }
 }
 
 
 // Remove a job from cron queue
 // Remove a job from cron queue
-func (c *cron) Remove(job QueJobInterface) error {
+func (c *cron) Remove(ctx context.Context, job QueJobInterface) error {
 	c.parseJobs()
 	c.parseJobs()
 	defer c.writeCronJob()
 	defer c.writeCronJob()
 
 
@@ -242,7 +242,7 @@ func (c *cron) Listen(_ []*QorJobDefinition, getJob func(qorJobID uint) (QueJobI
 			fmt.Println(err)
 			fmt.Println(err)
 			os.Exit(1)
 			os.Exit(1)
 		}
 		}
-		if err := c.doRunJob(job); err == nil {
+		if err := c.doRunJob(context.Background(), job); err == nil {
 			os.Exit(0)
 			os.Exit(0)
 		} else {
 		} else {
 			fmt.Println(err)
 			fmt.Println(err)
@@ -253,7 +253,7 @@ func (c *cron) Listen(_ []*QorJobDefinition, getJob func(qorJobID uint) (QueJobI
 	return nil
 	return nil
 }
 }
 
 
-func (c *cron) doRunJob(job QueJobInterface) error {
+func (c *cron) doRunJob(ctx context.Context, job QueJobInterface) error {
 	defer func() {
 	defer func() {
 		if r := recover(); r != nil {
 		if r := recover(); r != nil {
 			job.AddLog(string(debug.Stack()))
 			job.AddLog(string(debug.Stack()))
@@ -267,7 +267,7 @@ func (c *cron) doRunJob(job QueJobInterface) error {
 	}
 	}
 
 
 	if err := job.SetStatus(JobStatusRunning); err == nil {
 	if err := job.SetStatus(JobStatusRunning); err == nil {
-		if err := c.run(job); err == nil {
+		if err := c.run(ctx, job); err == nil {
 			return job.SetStatus(JobStatusDone)
 			return job.SetStatus(JobStatusDone)
 		}
 		}
 
 
@@ -277,3 +277,7 @@ func (c *cron) doRunJob(job QueJobInterface) error {
 
 
 	return nil
 	return nil
 }
 }
+
+func (c *cron) Shutdown(ctx context.Context) error {
+	return nil
+}

+ 21 - 11
worker/goque.go

@@ -12,12 +12,14 @@ import (
 
 
 	"github.com/tnclong/go-que"
 	"github.com/tnclong/go-que"
 	"github.com/tnclong/go-que/pg"
 	"github.com/tnclong/go-que/pg"
+	"go.uber.org/multierr"
 	"gorm.io/gorm"
 	"gorm.io/gorm"
 )
 )
 
 
 type goque struct {
 type goque struct {
-	q  que.Queue
-	db *gorm.DB
+	q   que.Queue
+	db  *gorm.DB
+	wks []*que.Worker
 }
 }
 
 
 func NewGoQueQueue(db *gorm.DB) Queue {
 func NewGoQueQueue(db *gorm.DB) Queue {
@@ -43,7 +45,7 @@ func NewGoQueQueue(db *gorm.DB) Queue {
 	}
 	}
 }
 }
 
 
-func (q *goque) Add(job QueJobInterface) error {
+func (q *goque) Add(ctx context.Context, job QueJobInterface) error {
 	jobInfo, err := job.GetJobInfo()
 	jobInfo, err := job.GetJobInfo()
 
 
 	if err != nil {
 	if err != nil {
@@ -55,7 +57,7 @@ func (q *goque) Add(job QueJobInterface) error {
 		job.SetStatus(JobStatusScheduled)
 		job.SetStatus(JobStatusScheduled)
 	}
 	}
 
 
-	_, err = q.q.Enqueue(context.Background(), nil, que.Plan{
+	_, err = q.q.Enqueue(ctx, nil, que.Plan{
 		Queue: "worker_" + jobInfo.JobName,
 		Queue: "worker_" + jobInfo.JobName,
 		Args:  que.Args(jobInfo.JobID, jobInfo.Argument),
 		Args:  que.Args(jobInfo.JobID, jobInfo.Argument),
 		RunAt: runAt,
 		RunAt: runAt,
@@ -74,11 +76,11 @@ func (q *goque) run(ctx context.Context, job QueJobInterface) error {
 	return job.GetHandler()(ctx, job)
 	return job.GetHandler()(ctx, job)
 }
 }
 
 
-func (q *goque) Kill(job QueJobInterface) error {
+func (q *goque) Kill(ctx context.Context, job QueJobInterface) error {
 	return job.SetStatus(JobStatusKilled)
 	return job.SetStatus(JobStatusKilled)
 }
 }
 
 
-func (q *goque) Remove(job QueJobInterface) error {
+func (q *goque) Remove(ctx context.Context, job QueJobInterface) error {
 	return job.SetStatus(JobStatusCancelled)
 	return job.SetStatus(JobStatusCancelled)
 }
 }
 
 
@@ -135,7 +137,7 @@ func (q *goque) Listen(jobDefs []*QorJobDefinition, getJob func(qorJobID uint) (
 					return err
 					return err
 				}
 				}
 
 
-				hctx, cf := context.WithCancel(context.Background())
+				hctx, cf := context.WithCancel(ctx)
 				hDoneC := make(chan struct{})
 				hDoneC := make(chan struct{})
 				isAborted := false
 				isAborted := false
 				go func() {
 				go func() {
@@ -177,13 +179,11 @@ func (q *goque) Listen(jobDefs []*QorJobDefinition, getJob func(qorJobID uint) (
 		if err != nil {
 		if err != nil {
 			panic(err)
 			panic(err)
 		}
 		}
-
+		q.wks = append(q.wks, worker)
 		go func() {
 		go func() {
 			if err := worker.Run(); err != nil {
 			if err := worker.Run(); err != nil {
-				errStr := fmt.Sprintf("worker Run() error: %s", err.Error())
-				fmt.Println(errStr)
 				q.db.Create(&GoQueError{
 				q.db.Create(&GoQueError{
-					Error: errStr,
+					Error: fmt.Sprintf("worker Run() error: %s", err.Error()),
 				})
 				})
 			}
 			}
 		}()
 		}()
@@ -192,6 +192,16 @@ func (q *goque) Listen(jobDefs []*QorJobDefinition, getJob func(qorJobID uint) (
 	return nil
 	return nil
 }
 }
 
 
+func (q *goque) Shutdown(ctx context.Context) error {
+	var errs error
+	for _, wk := range q.wks {
+		if err := wk.Stop(ctx); err != nil {
+			errs = multierr.Append(errs, err)
+		}
+	}
+	return errs
+}
+
 func (q *goque) parseArgs(data []byte, args ...interface{}) error {
 func (q *goque) parseArgs(data []byte, args ...interface{}) error {
 	d := json.NewDecoder(bytes.NewReader(data))
 	d := json.NewDecoder(bytes.NewReader(data))
 	if _, err := d.Token(); err != nil {
 	if _, err := d.Token(); err != nil {

+ 6 - 3
worker/integration_test/test_que.go

@@ -14,7 +14,7 @@ import (
 var items []worker.QueJobInterface
 var items []worker.QueJobInterface
 
 
 var Que = &mock.QueueMock{
 var Que = &mock.QueueMock{
-	AddFunc: func(job worker.QueJobInterface) error {
+	AddFunc: func(ctx context.Context, job worker.QueJobInterface) error {
 		jobInfo, err := job.GetJobInfo()
 		jobInfo, err := job.GetJobInfo()
 
 
 		if err != nil {
 		if err != nil {
@@ -26,15 +26,18 @@ var Que = &mock.QueueMock{
 		items = append(items, job)
 		items = append(items, job)
 		return nil
 		return nil
 	},
 	},
-	KillFunc: func(job worker.QueJobInterface) error {
+	KillFunc: func(ctx context.Context, job worker.QueJobInterface) error {
 		return job.SetStatus(worker.JobStatusKilled)
 		return job.SetStatus(worker.JobStatusKilled)
 	},
 	},
 	ListenFunc: func(jobDefs []*worker.QorJobDefinition, getJob func(qorJobID uint) (worker.QueJobInterface, error)) error {
 	ListenFunc: func(jobDefs []*worker.QorJobDefinition, getJob func(qorJobID uint) (worker.QueJobInterface, error)) error {
 		return nil
 		return nil
 	},
 	},
-	RemoveFunc: func(job worker.QueJobInterface) error {
+	RemoveFunc: func(ctx context.Context, job worker.QueJobInterface) error {
 		return job.SetStatus(worker.JobStatusCancelled)
 		return job.SetStatus(worker.JobStatusCancelled)
 	},
 	},
+	ShutdownFunc: func(ctx context.Context) error {
+		return nil
+	},
 }
 }
 
 
 func ConsumeQueItem() (err error) {
 func ConsumeQueItem() (err error) {

+ 97 - 34
worker/mock/queue.go

@@ -4,6 +4,7 @@
 package mock
 package mock
 
 
 import (
 import (
+	"context"
 	"github.com/qor5/admin/worker"
 	"github.com/qor5/admin/worker"
 	"sync"
 	"sync"
 )
 )
@@ -18,18 +19,21 @@ var _ worker.Queue = &QueueMock{}
 //
 //
 //		// make and configure a mocked worker.Queue
 //		// make and configure a mocked worker.Queue
 //		mockedQueue := &QueueMock{
 //		mockedQueue := &QueueMock{
-//			AddFunc: func(queJobInterface worker.QueJobInterface) error {
+//			AddFunc: func(ctx context.Context, job worker.QueJobInterface) error {
 //				panic("mock out the Add method")
 //				panic("mock out the Add method")
 //			},
 //			},
-//			KillFunc: func(queJobInterface worker.QueJobInterface) error {
+//			KillFunc: func(ctx context.Context, job worker.QueJobInterface) error {
 //				panic("mock out the Kill method")
 //				panic("mock out the Kill method")
 //			},
 //			},
 //			ListenFunc: func(jobDefs []*worker.QorJobDefinition, getJob func(qorJobID uint) (worker.QueJobInterface, error)) error {
 //			ListenFunc: func(jobDefs []*worker.QorJobDefinition, getJob func(qorJobID uint) (worker.QueJobInterface, error)) error {
 //				panic("mock out the Listen method")
 //				panic("mock out the Listen method")
 //			},
 //			},
-//			RemoveFunc: func(queJobInterface worker.QueJobInterface) error {
+//			RemoveFunc: func(ctx context.Context, job worker.QueJobInterface) error {
 //				panic("mock out the Remove method")
 //				panic("mock out the Remove method")
 //			},
 //			},
+//			ShutdownFunc: func(ctx context.Context) error {
+//				panic("mock out the Shutdown method")
+//			},
 //		}
 //		}
 //
 //
 //		// use mockedQueue in code that requires worker.Queue
 //		// use mockedQueue in code that requires worker.Queue
@@ -38,28 +42,35 @@ var _ worker.Queue = &QueueMock{}
 //	}
 //	}
 type QueueMock struct {
 type QueueMock struct {
 	// AddFunc mocks the Add method.
 	// AddFunc mocks the Add method.
-	AddFunc func(queJobInterface worker.QueJobInterface) error
+	AddFunc func(ctx context.Context, job worker.QueJobInterface) error
 
 
 	// KillFunc mocks the Kill method.
 	// KillFunc mocks the Kill method.
-	KillFunc func(queJobInterface worker.QueJobInterface) error
+	KillFunc func(ctx context.Context, job worker.QueJobInterface) error
 
 
 	// ListenFunc mocks the Listen method.
 	// ListenFunc mocks the Listen method.
 	ListenFunc func(jobDefs []*worker.QorJobDefinition, getJob func(qorJobID uint) (worker.QueJobInterface, error)) error
 	ListenFunc func(jobDefs []*worker.QorJobDefinition, getJob func(qorJobID uint) (worker.QueJobInterface, error)) error
 
 
 	// RemoveFunc mocks the Remove method.
 	// RemoveFunc mocks the Remove method.
-	RemoveFunc func(queJobInterface worker.QueJobInterface) error
+	RemoveFunc func(ctx context.Context, job worker.QueJobInterface) error
+
+	// ShutdownFunc mocks the Shutdown method.
+	ShutdownFunc func(ctx context.Context) error
 
 
 	// calls tracks calls to the methods.
 	// calls tracks calls to the methods.
 	calls struct {
 	calls struct {
 		// Add holds details about calls to the Add method.
 		// Add holds details about calls to the Add method.
 		Add []struct {
 		Add []struct {
-			// QueJobInterface is the queJobInterface argument value.
-			QueJobInterface worker.QueJobInterface
+			// Ctx is the ctx argument value.
+			Ctx context.Context
+			// Job is the job argument value.
+			Job worker.QueJobInterface
 		}
 		}
 		// Kill holds details about calls to the Kill method.
 		// Kill holds details about calls to the Kill method.
 		Kill []struct {
 		Kill []struct {
-			// QueJobInterface is the queJobInterface argument value.
-			QueJobInterface worker.QueJobInterface
+			// Ctx is the ctx argument value.
+			Ctx context.Context
+			// Job is the job argument value.
+			Job worker.QueJobInterface
 		}
 		}
 		// Listen holds details about calls to the Listen method.
 		// Listen holds details about calls to the Listen method.
 		Listen []struct {
 		Listen []struct {
@@ -70,30 +81,40 @@ type QueueMock struct {
 		}
 		}
 		// Remove holds details about calls to the Remove method.
 		// Remove holds details about calls to the Remove method.
 		Remove []struct {
 		Remove []struct {
-			// QueJobInterface is the queJobInterface argument value.
-			QueJobInterface worker.QueJobInterface
+			// Ctx is the ctx argument value.
+			Ctx context.Context
+			// Job is the job argument value.
+			Job worker.QueJobInterface
+		}
+		// Shutdown holds details about calls to the Shutdown method.
+		Shutdown []struct {
+			// Ctx is the ctx argument value.
+			Ctx context.Context
 		}
 		}
 	}
 	}
-	lockAdd    sync.RWMutex
-	lockKill   sync.RWMutex
-	lockListen sync.RWMutex
-	lockRemove sync.RWMutex
+	lockAdd      sync.RWMutex
+	lockKill     sync.RWMutex
+	lockListen   sync.RWMutex
+	lockRemove   sync.RWMutex
+	lockShutdown sync.RWMutex
 }
 }
 
 
 // Add calls AddFunc.
 // Add calls AddFunc.
-func (mock *QueueMock) Add(queJobInterface worker.QueJobInterface) error {
+func (mock *QueueMock) Add(ctx context.Context, job worker.QueJobInterface) error {
 	if mock.AddFunc == nil {
 	if mock.AddFunc == nil {
 		panic("QueueMock.AddFunc: method is nil but Queue.Add was just called")
 		panic("QueueMock.AddFunc: method is nil but Queue.Add was just called")
 	}
 	}
 	callInfo := struct {
 	callInfo := struct {
-		QueJobInterface worker.QueJobInterface
+		Ctx context.Context
+		Job worker.QueJobInterface
 	}{
 	}{
-		QueJobInterface: queJobInterface,
+		Ctx: ctx,
+		Job: job,
 	}
 	}
 	mock.lockAdd.Lock()
 	mock.lockAdd.Lock()
 	mock.calls.Add = append(mock.calls.Add, callInfo)
 	mock.calls.Add = append(mock.calls.Add, callInfo)
 	mock.lockAdd.Unlock()
 	mock.lockAdd.Unlock()
-	return mock.AddFunc(queJobInterface)
+	return mock.AddFunc(ctx, job)
 }
 }
 
 
 // AddCalls gets all the calls that were made to Add.
 // AddCalls gets all the calls that were made to Add.
@@ -101,10 +122,12 @@ func (mock *QueueMock) Add(queJobInterface worker.QueJobInterface) error {
 //
 //
 //	len(mockedQueue.AddCalls())
 //	len(mockedQueue.AddCalls())
 func (mock *QueueMock) AddCalls() []struct {
 func (mock *QueueMock) AddCalls() []struct {
-	QueJobInterface worker.QueJobInterface
+	Ctx context.Context
+	Job worker.QueJobInterface
 } {
 } {
 	var calls []struct {
 	var calls []struct {
-		QueJobInterface worker.QueJobInterface
+		Ctx context.Context
+		Job worker.QueJobInterface
 	}
 	}
 	mock.lockAdd.RLock()
 	mock.lockAdd.RLock()
 	calls = mock.calls.Add
 	calls = mock.calls.Add
@@ -113,19 +136,21 @@ func (mock *QueueMock) AddCalls() []struct {
 }
 }
 
 
 // Kill calls KillFunc.
 // Kill calls KillFunc.
-func (mock *QueueMock) Kill(queJobInterface worker.QueJobInterface) error {
+func (mock *QueueMock) Kill(ctx context.Context, job worker.QueJobInterface) error {
 	if mock.KillFunc == nil {
 	if mock.KillFunc == nil {
 		panic("QueueMock.KillFunc: method is nil but Queue.Kill was just called")
 		panic("QueueMock.KillFunc: method is nil but Queue.Kill was just called")
 	}
 	}
 	callInfo := struct {
 	callInfo := struct {
-		QueJobInterface worker.QueJobInterface
+		Ctx context.Context
+		Job worker.QueJobInterface
 	}{
 	}{
-		QueJobInterface: queJobInterface,
+		Ctx: ctx,
+		Job: job,
 	}
 	}
 	mock.lockKill.Lock()
 	mock.lockKill.Lock()
 	mock.calls.Kill = append(mock.calls.Kill, callInfo)
 	mock.calls.Kill = append(mock.calls.Kill, callInfo)
 	mock.lockKill.Unlock()
 	mock.lockKill.Unlock()
-	return mock.KillFunc(queJobInterface)
+	return mock.KillFunc(ctx, job)
 }
 }
 
 
 // KillCalls gets all the calls that were made to Kill.
 // KillCalls gets all the calls that were made to Kill.
@@ -133,10 +158,12 @@ func (mock *QueueMock) Kill(queJobInterface worker.QueJobInterface) error {
 //
 //
 //	len(mockedQueue.KillCalls())
 //	len(mockedQueue.KillCalls())
 func (mock *QueueMock) KillCalls() []struct {
 func (mock *QueueMock) KillCalls() []struct {
-	QueJobInterface worker.QueJobInterface
+	Ctx context.Context
+	Job worker.QueJobInterface
 } {
 } {
 	var calls []struct {
 	var calls []struct {
-		QueJobInterface worker.QueJobInterface
+		Ctx context.Context
+		Job worker.QueJobInterface
 	}
 	}
 	mock.lockKill.RLock()
 	mock.lockKill.RLock()
 	calls = mock.calls.Kill
 	calls = mock.calls.Kill
@@ -181,19 +208,21 @@ func (mock *QueueMock) ListenCalls() []struct {
 }
 }
 
 
 // Remove calls RemoveFunc.
 // Remove calls RemoveFunc.
-func (mock *QueueMock) Remove(queJobInterface worker.QueJobInterface) error {
+func (mock *QueueMock) Remove(ctx context.Context, job worker.QueJobInterface) error {
 	if mock.RemoveFunc == nil {
 	if mock.RemoveFunc == nil {
 		panic("QueueMock.RemoveFunc: method is nil but Queue.Remove was just called")
 		panic("QueueMock.RemoveFunc: method is nil but Queue.Remove was just called")
 	}
 	}
 	callInfo := struct {
 	callInfo := struct {
-		QueJobInterface worker.QueJobInterface
+		Ctx context.Context
+		Job worker.QueJobInterface
 	}{
 	}{
-		QueJobInterface: queJobInterface,
+		Ctx: ctx,
+		Job: job,
 	}
 	}
 	mock.lockRemove.Lock()
 	mock.lockRemove.Lock()
 	mock.calls.Remove = append(mock.calls.Remove, callInfo)
 	mock.calls.Remove = append(mock.calls.Remove, callInfo)
 	mock.lockRemove.Unlock()
 	mock.lockRemove.Unlock()
-	return mock.RemoveFunc(queJobInterface)
+	return mock.RemoveFunc(ctx, job)
 }
 }
 
 
 // RemoveCalls gets all the calls that were made to Remove.
 // RemoveCalls gets all the calls that were made to Remove.
@@ -201,13 +230,47 @@ func (mock *QueueMock) Remove(queJobInterface worker.QueJobInterface) error {
 //
 //
 //	len(mockedQueue.RemoveCalls())
 //	len(mockedQueue.RemoveCalls())
 func (mock *QueueMock) RemoveCalls() []struct {
 func (mock *QueueMock) RemoveCalls() []struct {
-	QueJobInterface worker.QueJobInterface
+	Ctx context.Context
+	Job worker.QueJobInterface
 } {
 } {
 	var calls []struct {
 	var calls []struct {
-		QueJobInterface worker.QueJobInterface
+		Ctx context.Context
+		Job worker.QueJobInterface
 	}
 	}
 	mock.lockRemove.RLock()
 	mock.lockRemove.RLock()
 	calls = mock.calls.Remove
 	calls = mock.calls.Remove
 	mock.lockRemove.RUnlock()
 	mock.lockRemove.RUnlock()
 	return calls
 	return calls
 }
 }
+
+// Shutdown calls ShutdownFunc.
+func (mock *QueueMock) Shutdown(ctx context.Context) error {
+	if mock.ShutdownFunc == nil {
+		panic("QueueMock.ShutdownFunc: method is nil but Queue.Shutdown was just called")
+	}
+	callInfo := struct {
+		Ctx context.Context
+	}{
+		Ctx: ctx,
+	}
+	mock.lockShutdown.Lock()
+	mock.calls.Shutdown = append(mock.calls.Shutdown, callInfo)
+	mock.lockShutdown.Unlock()
+	return mock.ShutdownFunc(ctx)
+}
+
+// ShutdownCalls gets all the calls that were made to Shutdown.
+// Check the length with:
+//
+//	len(mockedQueue.ShutdownCalls())
+func (mock *QueueMock) ShutdownCalls() []struct {
+	Ctx context.Context
+} {
+	var calls []struct {
+		Ctx context.Context
+	}
+	mock.lockShutdown.RLock()
+	calls = mock.calls.Shutdown
+	mock.lockShutdown.RUnlock()
+	return calls
+}

+ 6 - 3
worker/queue.go

@@ -1,5 +1,7 @@
 package worker
 package worker
 
 
+import "context"
+
 //go:generate moq -pkg mock -out mock/queue.go . Queue
 //go:generate moq -pkg mock -out mock/queue.go . Queue
 
 
 type QorJobDefinition struct {
 type QorJobDefinition struct {
@@ -8,8 +10,9 @@ type QorJobDefinition struct {
 }
 }
 
 
 type Queue interface {
 type Queue interface {
-	Add(QueJobInterface) error
-	Kill(QueJobInterface) error
-	Remove(QueJobInterface) error
+	Add(ctx context.Context, job QueJobInterface) error
+	Kill(ctx context.Context, job QueJobInterface) error
+	Remove(ctx context.Context, job QueJobInterface) error
 	Listen(jobDefs []*QorJobDefinition, getJob func(qorJobID uint) (QueJobInterface, error)) error
 	Listen(jobDefs []*QorJobDefinition, getJob func(qorJobID uint) (QueJobInterface, error)) error
+	Shutdown(ctx context.Context) error
 }
 }