Jelajahi Sumber

Merge branch 'main' into fix-security-issues

Charles Shen 1 tahun lalu
induk
melakukan
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/*
 ARG GITHUB_TOKEN
 WORKDIR /qor5

+ 24 - 24
example/admin/data_init.go

@@ -84,7 +84,7 @@ func InitDB(db *gorm.DB, tables []string) {
 	}
 	// Seq
 	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)
 		}
 	}
@@ -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
 --
 
-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
 --
 
-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 (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"`
 
-	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"`
 
@@ -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);
 `
 
-	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"`
 

+ 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/*
 WORKDIR /qor5
 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/*
 WORKDIR /qor5
 COPY . .

+ 2 - 2
go.mod

@@ -25,7 +25,7 @@ require (
 	github.com/pquerna/otp v1.4.0
 	github.com/qor/oss v0.0.0-20230717083721-c04686f83630
 	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/sunfmin/reflectutils v1.0.3
 	github.com/theplant/bimg v1.1.1
@@ -37,6 +37,7 @@ require (
 	github.com/tnclong/go-que v0.0.0-20201111043106-1fc5fa2b9761
 	github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f
 	github.com/wcharczuk/go-chart/v2 v2.1.0
+	go.uber.org/multierr v1.11.0
 	go.uber.org/zap v1.24.0
 	goji.io v2.0.2+incompatible
 	golang.org/x/text v0.9.0
@@ -90,7 +91,6 @@ require (
 	github.com/therootcompany/xz v1.0.1 // indirect
 	github.com/ulikunitz/xz v0.5.11 // 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
 	golang.org/x/crypto v0.9.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/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/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/go.mod h1:Zfy7B3X5DnQSud0HTV4h/ih5TTQgaT2NWwuSIRGLdcM=
 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
 	if !handled {
 		err = media.Store(media.URL(), option, mediaFile)
-		return false, err
+		return true, err
 	}
 
 	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()))
 
 	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.Field("SelectContainer").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) 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())
 		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
 		}
 		totalCount = int(c)
@@ -1743,7 +1760,7 @@ func sharedContainerSearcher(db *gorm.DB, mb *presets.ModelBuilder) presets.Sear
 			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
 		}
 		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"]
 	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{})
 	return
@@ -758,8 +758,8 @@ func (b *Builder) localizeContainersToAnotherPage(db *gorm.DB, pageID int, pageV
 			newModelID = reflectutils.MustGet(model, "ID").(uint)
 		} else {
 			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
 			}
 
@@ -776,24 +776,29 @@ func (b *Builder) localizeContainersToAnotherPage(db *gorm.DB, pageID int, pageV
 				}
 				newModelID = reflectutils.MustGet(model, "ID").(uint)
 			} 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
 		}
 	}
@@ -865,8 +870,12 @@ func (b *Builder) RenameContainerDialog(ctx *web.EventContext) (r web.EventRespo
 	okAction := web.Plaid().
 		URL(fmt.Sprintf("%s/editors", b.prefix)).
 		EventFunc(RenameContainerEvent).Query(paramContainerID, paramID).Go()
+	portalName := dialogPortalName
+	if ctx.R.FormValue("portal") == "presets" {
+		portalName = presets.DialogPortalName
+	}
 	r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
-		Name: dialogPortalName,
+		Name: portalName,
 		Body: web.Scope(
 			VDialog(
 				VCard(

+ 4 - 0
pagebuilder/messages.go

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

+ 1 - 0
pagebuilder/models.go

@@ -141,6 +141,7 @@ type Container struct {
 	DisplayName  string
 
 	l10n.Locale
+	LocalizeFromModelID uint
 }
 
 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"]
 		paramID := ctx.R.FormValue(presets.ParamID)
 		me := mb.Editing()
-		vErr := me.RunSetterFunc(ctx, false, toObj)
-		if vErr.HaveErrors() {
-			presets.ShowMessage(&r, vErr.Error(), "error")
-			return
-		}
 
 		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 {
 			presets.ShowMessage(&r, err.Error(), "error")
 			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 {
 			presets.ShowMessage(&r, err.Error(), "error")
 			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) {
 	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)
@@ -93,6 +116,8 @@ func (collection *Collection) EditingComponentFunc(obj interface{}, field *prese
 		setting.Title = modelSetting.GetTitle()
 		setting.Description = modelSetting.GetDescription()
 		setting.Keywords = modelSetting.GetKeywords()
+		setting.OpenGraphTitle = modelSetting.GetOpenGraphTitle()
+		setting.OpenGraphDescription = modelSetting.GetOpenGraphDescription()
 		setting.OpenGraphURL = modelSetting.GetOpenGraphURL()
 		setting.OpenGraphType = modelSetting.GetOpenGraphType()
 		setting.OpenGraphImageURL = modelSetting.GetOpenGraphImageURL()
@@ -207,7 +232,6 @@ func (collection *Collection) pageFunc(ctx *web.EventContext) (_ web.PageRespons
 		)
 		seoComponents = append(seoComponents, comp)
 	}
-
 	return web.PageResponse{
 		PageTitle: msgr.PageTitle,
 		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)
 		db   = collection.getDBFromContext(req.Context())
 	)
-
 	if seo.name == collection.globalName {
 		seos = append(seos, seo)
 	} else {
@@ -284,9 +307,9 @@ func (collection *Collection) vseo(fieldPrefix string, seo *SEO, setting *Settin
 		),
 		VCard(
 			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),
 
@@ -294,11 +317,15 @@ func (collection *Collection) vseo(fieldPrefix string, seo *SEO, setting *Settin
 		VCard(
 			VCardText(
 				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(
-					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(
 					VCol(views.QMediaBox(db).Label(msgr.OpenGraphImage).
@@ -321,6 +348,9 @@ func (collection *Collection) vseo(fieldPrefix string, seo *SEO, setting *Settin
 								},
 							},
 						})).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),
 	).Attr("ref", "seo")
@@ -348,24 +378,33 @@ func (collection *Collection) save(ctx *web.EventContext) (r web.EventResponse,
 		if !strings.HasPrefix(fieldWithPrefix, name) {
 			continue
 		}
-
 		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" {
 				mediaBox.Description = ctx.R.FormValue(fieldWithPrefix)
 				if err != nil {
 					return
 				}
+				settingVals["OpenGraphImageFromMediaLibrary"] = mediaBox
 			}
-		} else if strings.HasPrefix(field, "Variables") {
+			continue
+		}
+		if strings.HasPrefix(field, "Variables") {
 			key := strings.Replace(field, "Variables.", "", -1)
 			variables[key] = ctx.R.FormValue(fieldWithPrefix)
 		} else {
@@ -374,6 +413,14 @@ func (collection *Collection) save(ctx *web.EventContext) (r web.EventResponse,
 	}
 	s := setting.GetSEOSetting()
 	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)
 		if err != nil {
 			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 == "" {
 			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 == "" {
 			setting.OpenGraphURL = s.OpenGraphURL
 		}
@@ -374,6 +380,19 @@ func replaceVariables(setting Setting, values map[string]string) Setting {
 	setting.Title = replace(setting.Title)
 	setting.Description = replace(setting.Description)
 	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
 }
 

+ 9 - 0
seo/messages.go

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

+ 49 - 4
seo/model.go

@@ -1,8 +1,11 @@
 package seo
 
 import (
+	"bytes"
 	"database/sql/driver"
+	"encoding/csv"
 	"encoding/json"
+	"strings"
 	"time"
 
 	"github.com/qor5/admin/l10n"
@@ -24,6 +27,8 @@ type QorSEOSettingInterface interface {
 	GetTitle() string
 	GetDescription() string
 	GetKeywords() string
+	GetOpenGraphTitle() string
+	GetOpenGraphDescription() string
 	GetOpenGraphURL() string
 	GetOpenGraphType() string
 	GetOpenGraphImageURL() string
@@ -49,6 +54,8 @@ type Setting struct {
 	Title                          string `gorm:"size:4294967295"`
 	Description                    string
 	Keywords                       string
+	OpenGraphTitle                 string
+	OpenGraphDescription           string
 	OpenGraphURL                   string
 	OpenGraphType                  string
 	OpenGraphImageURL              string
@@ -102,6 +109,12 @@ func (s *QorSEOSetting) SetName(name string) {
 	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 {
 	return s.Setting.OpenGraphURL
 }
@@ -154,7 +167,10 @@ func (setting Setting) Value() (driver.Value, error) {
 }
 
 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
@@ -181,8 +197,8 @@ func (setting Variables) Value() (driver.Value, error) {
 
 func (setting Setting) HTMLComponent(tags map[string]string) h.HTMLComponent {
 	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:type":        setting.OpenGraphType,
 		"og:image":       setting.OpenGraphImageURL,
@@ -192,7 +208,7 @@ func (setting Setting) HTMLComponent(tags map[string]string) h.HTMLComponent {
 		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, ok := tags[key]; ok {
 				openGraphData[key] = v
@@ -222,3 +238,32 @@ func (setting Setting) HTMLComponent(tags map[string]string) h.HTMLComponent {
 		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",
 			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{},
 			want: `
 			<title>title</title>
 			<meta name='description' content='description'>
 			<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: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'>`,
 		},
 
 		{
 			name: "Render the seo html using the tag data",
 			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{
 				"og:type":       "product",
@@ -52,10 +56,10 @@ func TestSettingHTMLComponent(t *testing.T) {
 			<title>title</title>
 			<meta name='description' content='description'>
 			<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: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='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 {
 	case JobStatusRunning:
-		err = b.q.Kill(inst)
+		err = b.q.Kill(ctx.R.Context(), inst)
 	case JobStatusNew, JobStatusScheduled:
-		err = b.q.Remove(inst)
+		err = b.q.Remove(ctx.R.Context(), inst)
 	}
 
 	return er, err

+ 13 - 8
worker/builder.go

@@ -1,6 +1,7 @@
 package worker
 
 import (
+	"context"
 	"encoding/json"
 	"errors"
 	"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) {
 	if err = editIsAllowed(ctx.R, qorJob.Job); err != nil {
 		return
@@ -439,7 +444,7 @@ func (b *Builder) createJob(ctx *web.EventContext, qorJob *QorJob) (j *QorJob, e
 		if err != nil {
 			return err
 		}
-		return b.q.Add(inst)
+		return b.q.Add(ctx.R.Context(), inst)
 	})
 	return
 }
@@ -477,7 +482,7 @@ func (b *Builder) eventAbortJob(ctx *web.EventContext) (er web.EventResponse, er
 	}
 	isScheduled := inst.Status == JobStatusScheduled
 
-	err = b.doAbortJob(inst)
+	err = b.doAbortJob(ctx.R.Context(), inst)
 	if err != nil {
 		_, ok := err.(*cannotAbortError)
 		if !ok {
@@ -517,12 +522,12 @@ func (e *cannotAbortError) Error() string {
 	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 {
 	case JobStatusRunning:
-		return b.q.Kill(inst)
+		return b.q.Kill(ctx, inst)
 	case JobStatusNew, JobStatusScheduled:
-		return b.q.Remove(inst)
+		return b.q.Remove(ctx, inst)
 	default:
 		return &cannotAbortError{
 			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 {
 		return er, err
 	}
-	err = b.q.Add(inst)
+	err = b.q.Add(ctx.R.Context(), inst)
 	if err != nil {
 		return er, err
 	}
@@ -604,7 +609,7 @@ func (b *Builder) eventUpdateJob(ctx *web.EventContext) (er web.EventResponse, e
 		return er, err
 	}
 	oldArgs, _ := jb.parseArgs(old.Args)
-	err = b.doAbortJob(old)
+	err = b.doAbortJob(ctx.R.Context(), old)
 	if err != nil {
 		_, ok := err.(*cannotAbortError)
 		if !ok {
@@ -624,7 +629,7 @@ func (b *Builder) eventUpdateJob(ctx *web.EventContext) (er web.EventResponse, e
 	if err != nil {
 		return er, err
 	}
-	err = b.q.Add(newInst)
+	err = b.q.Add(ctx.R.Context(), newInst)
 	if err != nil {
 		return er, err
 	}

+ 12 - 8
worker/cron.go

@@ -94,7 +94,7 @@ func (c *cron) writeCronJob() error {
 }
 
 // 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()
 	defer c.writeCronJob()
 
@@ -135,7 +135,7 @@ func (c *cron) Add(job QueJobInterface) (err error) {
 }
 
 // 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()
 	if err != nil {
 		return err
@@ -166,7 +166,7 @@ func (c *cron) run(qorJob QueJobInterface) (err error) {
 	qorJob.StartRefresh()
 	defer qorJob.StopRefresh()
 
-	err = h(context.Background(), qorJob)
+	err = h(ctx, qorJob)
 	if err == nil {
 		c.parseJobs()
 		defer c.writeCronJob()
@@ -180,7 +180,7 @@ func (c *cron) run(qorJob QueJobInterface) (err error) {
 }
 
 // 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()
 	defer c.writeCronJob()
 
@@ -204,7 +204,7 @@ func (c *cron) Kill(job QueJobInterface) (err error) {
 }
 
 // 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()
 	defer c.writeCronJob()
 
@@ -242,7 +242,7 @@ func (c *cron) Listen(_ []*QorJobDefinition, getJob func(qorJobID uint) (QueJobI
 			fmt.Println(err)
 			os.Exit(1)
 		}
-		if err := c.doRunJob(job); err == nil {
+		if err := c.doRunJob(context.Background(), job); err == nil {
 			os.Exit(0)
 		} else {
 			fmt.Println(err)
@@ -253,7 +253,7 @@ func (c *cron) Listen(_ []*QorJobDefinition, getJob func(qorJobID uint) (QueJobI
 	return nil
 }
 
-func (c *cron) doRunJob(job QueJobInterface) error {
+func (c *cron) doRunJob(ctx context.Context, job QueJobInterface) error {
 	defer func() {
 		if r := recover(); r != nil {
 			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 := c.run(job); err == nil {
+		if err := c.run(ctx, job); err == nil {
 			return job.SetStatus(JobStatusDone)
 		}
 
@@ -277,3 +277,7 @@ func (c *cron) doRunJob(job QueJobInterface) error {
 
 	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/pg"
+	"go.uber.org/multierr"
 	"gorm.io/gorm"
 )
 
 type goque struct {
-	q  que.Queue
-	db *gorm.DB
+	q   que.Queue
+	db  *gorm.DB
+	wks []*que.Worker
 }
 
 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()
 
 	if err != nil {
@@ -55,7 +57,7 @@ func (q *goque) Add(job QueJobInterface) error {
 		job.SetStatus(JobStatusScheduled)
 	}
 
-	_, err = q.q.Enqueue(context.Background(), nil, que.Plan{
+	_, err = q.q.Enqueue(ctx, nil, que.Plan{
 		Queue: "worker_" + jobInfo.JobName,
 		Args:  que.Args(jobInfo.JobID, jobInfo.Argument),
 		RunAt: runAt,
@@ -74,11 +76,11 @@ func (q *goque) run(ctx context.Context, job QueJobInterface) error {
 	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)
 }
 
-func (q *goque) Remove(job QueJobInterface) error {
+func (q *goque) Remove(ctx context.Context, job QueJobInterface) error {
 	return job.SetStatus(JobStatusCancelled)
 }
 
@@ -135,7 +137,7 @@ func (q *goque) Listen(jobDefs []*QorJobDefinition, getJob func(qorJobID uint) (
 					return err
 				}
 
-				hctx, cf := context.WithCancel(context.Background())
+				hctx, cf := context.WithCancel(ctx)
 				hDoneC := make(chan struct{})
 				isAborted := false
 				go func() {
@@ -177,13 +179,11 @@ func (q *goque) Listen(jobDefs []*QorJobDefinition, getJob func(qorJobID uint) (
 		if err != nil {
 			panic(err)
 		}
-
+		q.wks = append(q.wks, worker)
 		go func() {
 			if err := worker.Run(); err != nil {
-				errStr := fmt.Sprintf("worker Run() error: %s", err.Error())
-				fmt.Println(errStr)
 				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
 }
 
+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 {
 	d := json.NewDecoder(bytes.NewReader(data))
 	if _, err := d.Token(); err != nil {

+ 6 - 3
worker/integration_test/test_que.go

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

+ 97 - 34
worker/mock/queue.go

@@ -4,6 +4,7 @@
 package mock
 
 import (
+	"context"
 	"github.com/qor5/admin/worker"
 	"sync"
 )
@@ -18,18 +19,21 @@ var _ worker.Queue = &QueueMock{}
 //
 //		// make and configure a mocked worker.Queue
 //		mockedQueue := &QueueMock{
-//			AddFunc: func(queJobInterface worker.QueJobInterface) error {
+//			AddFunc: func(ctx context.Context, job worker.QueJobInterface) error {
 //				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")
 //			},
 //			ListenFunc: func(jobDefs []*worker.QorJobDefinition, getJob func(qorJobID uint) (worker.QueJobInterface, error)) error {
 //				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")
 //			},
+//			ShutdownFunc: func(ctx context.Context) error {
+//				panic("mock out the Shutdown method")
+//			},
 //		}
 //
 //		// use mockedQueue in code that requires worker.Queue
@@ -38,28 +42,35 @@ var _ worker.Queue = &QueueMock{}
 //	}
 type QueueMock struct {
 	// 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 func(queJobInterface worker.QueJobInterface) error
+	KillFunc func(ctx context.Context, job worker.QueJobInterface) error
 
 	// ListenFunc mocks the Listen method.
 	ListenFunc func(jobDefs []*worker.QorJobDefinition, getJob func(qorJobID uint) (worker.QueJobInterface, error)) error
 
 	// 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 struct {
 		// Add holds details about calls to the Add method.
 		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 []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 []struct {
@@ -70,30 +81,40 @@ type QueueMock struct {
 		}
 		// Remove holds details about calls to the Remove method.
 		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.
-func (mock *QueueMock) Add(queJobInterface worker.QueJobInterface) error {
+func (mock *QueueMock) Add(ctx context.Context, job worker.QueJobInterface) error {
 	if mock.AddFunc == nil {
 		panic("QueueMock.AddFunc: method is nil but Queue.Add was just called")
 	}
 	callInfo := struct {
-		QueJobInterface worker.QueJobInterface
+		Ctx context.Context
+		Job worker.QueJobInterface
 	}{
-		QueJobInterface: queJobInterface,
+		Ctx: ctx,
+		Job: job,
 	}
 	mock.lockAdd.Lock()
 	mock.calls.Add = append(mock.calls.Add, callInfo)
 	mock.lockAdd.Unlock()
-	return mock.AddFunc(queJobInterface)
+	return mock.AddFunc(ctx, job)
 }
 
 // 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())
 func (mock *QueueMock) AddCalls() []struct {
-	QueJobInterface worker.QueJobInterface
+	Ctx context.Context
+	Job worker.QueJobInterface
 } {
 	var calls []struct {
-		QueJobInterface worker.QueJobInterface
+		Ctx context.Context
+		Job worker.QueJobInterface
 	}
 	mock.lockAdd.RLock()
 	calls = mock.calls.Add
@@ -113,19 +136,21 @@ func (mock *QueueMock) AddCalls() []struct {
 }
 
 // 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 {
 		panic("QueueMock.KillFunc: method is nil but Queue.Kill was just called")
 	}
 	callInfo := struct {
-		QueJobInterface worker.QueJobInterface
+		Ctx context.Context
+		Job worker.QueJobInterface
 	}{
-		QueJobInterface: queJobInterface,
+		Ctx: ctx,
+		Job: job,
 	}
 	mock.lockKill.Lock()
 	mock.calls.Kill = append(mock.calls.Kill, callInfo)
 	mock.lockKill.Unlock()
-	return mock.KillFunc(queJobInterface)
+	return mock.KillFunc(ctx, job)
 }
 
 // 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())
 func (mock *QueueMock) KillCalls() []struct {
-	QueJobInterface worker.QueJobInterface
+	Ctx context.Context
+	Job worker.QueJobInterface
 } {
 	var calls []struct {
-		QueJobInterface worker.QueJobInterface
+		Ctx context.Context
+		Job worker.QueJobInterface
 	}
 	mock.lockKill.RLock()
 	calls = mock.calls.Kill
@@ -181,19 +208,21 @@ func (mock *QueueMock) ListenCalls() []struct {
 }
 
 // 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 {
 		panic("QueueMock.RemoveFunc: method is nil but Queue.Remove was just called")
 	}
 	callInfo := struct {
-		QueJobInterface worker.QueJobInterface
+		Ctx context.Context
+		Job worker.QueJobInterface
 	}{
-		QueJobInterface: queJobInterface,
+		Ctx: ctx,
+		Job: job,
 	}
 	mock.lockRemove.Lock()
 	mock.calls.Remove = append(mock.calls.Remove, callInfo)
 	mock.lockRemove.Unlock()
-	return mock.RemoveFunc(queJobInterface)
+	return mock.RemoveFunc(ctx, job)
 }
 
 // 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())
 func (mock *QueueMock) RemoveCalls() []struct {
-	QueJobInterface worker.QueJobInterface
+	Ctx context.Context
+	Job worker.QueJobInterface
 } {
 	var calls []struct {
-		QueJobInterface worker.QueJobInterface
+		Ctx context.Context
+		Job worker.QueJobInterface
 	}
 	mock.lockRemove.RLock()
 	calls = mock.calls.Remove
 	mock.lockRemove.RUnlock()
 	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
 
+import "context"
+
 //go:generate moq -pkg mock -out mock/queue.go . Queue
 
 type QorJobDefinition struct {
@@ -8,8 +10,9 @@ type QorJobDefinition struct {
 }
 
 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
+	Shutdown(ctx context.Context) error
 }