diff --git a/services/headless-lms/migrations/20240823123921_add_code_giveaways.up.sql b/services/headless-lms/migrations/20240823123921_add_code_giveaways.up.sql index d93c6425859..300e9b01c2c 100644 --- a/services/headless-lms/migrations/20240823123921_add_code_giveaways.up.sql +++ b/services/headless-lms/migrations/20240823123921_add_code_giveaways.up.sql @@ -28,12 +28,15 @@ CREATE TABLE code_giveaway_codes ( code_giveaway_id UUID NOT NULL REFERENCES code_giveaways(id), code_given_to_user_id UUID REFERENCES users(id), added_by_user_id UUID NOT NULL REFERENCES users(id), - code VARCHAR(2048) NOT NULL + code VARCHAR(2048) NOT NULL, + -- No duplicate codes in a giveaway + UNIQUE NULLS NOT DISTINCT (code_giveaway_id, code, deleted_at) ); -- A user can only receive one code from a giveaway. We use unique index here because if we used a unique constraint we would like to have NULLS NOT disctinct on the delted_at column but NULLS DISTINCT on the code_given_to_user_id column. This did not seem possible so we use a unique index instead. CREATE UNIQUE INDEX giveaway_codes_one_code_per_user ON code_giveaway_codes (code_giveaway_id, code_given_to_user_id) WHERE deleted_at IS NULL; + CREATE TRIGGER set_timestamp BEFORE UPDATE ON code_giveaway_codes FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); COMMENT ON TABLE code_giveaway_codes IS 'A code that is available in a code giveaway. A user can only receive one code from a giveaway.'; diff --git a/services/headless-lms/models/.sqlx/query-28b1b1b396225e973197fc1e6dceefbaeab6f4da8a96c4ced3824c3f55217b94.json b/services/headless-lms/models/.sqlx/query-2da28ac86eded41b3dd098e00ed3f1dd1f8f15d402b358ac5fd6c77c5269784c.json similarity index 86% rename from services/headless-lms/models/.sqlx/query-28b1b1b396225e973197fc1e6dceefbaeab6f4da8a96c4ced3824c3f55217b94.json rename to services/headless-lms/models/.sqlx/query-2da28ac86eded41b3dd098e00ed3f1dd1f8f15d402b358ac5fd6c77c5269784c.json index 53c142bf5da..890ada2b1bb 100644 --- a/services/headless-lms/models/.sqlx/query-28b1b1b396225e973197fc1e6dceefbaeab6f4da8a96c4ced3824c3f55217b94.json +++ b/services/headless-lms/models/.sqlx/query-2da28ac86eded41b3dd098e00ed3f1dd1f8f15d402b358ac5fd6c77c5269784c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT *\nFROM code_giveaway_codes\nWHERE code_giveaway_id = $1\n AND deleted_at IS NULL\n ", + "query": "\nSELECT *\nFROM code_giveaway_codes\nWHERE code_giveaway_id = $1\n AND deleted_at IS NULL\n AND code_given_to_user_id IS NOT NULL\n ", "describe": { "columns": [ { @@ -49,5 +49,5 @@ }, "nullable": [false, false, false, true, false, true, false, false] }, - "hash": "28b1b1b396225e973197fc1e6dceefbaeab6f4da8a96c4ced3824c3f55217b94" + "hash": "2da28ac86eded41b3dd098e00ed3f1dd1f8f15d402b358ac5fd6c77c5269784c" } diff --git a/services/headless-lms/models/src/code_giveaway_codes.rs b/services/headless-lms/models/src/code_giveaway_codes.rs index 9b0e52ce230..dcb5826f77a 100644 --- a/services/headless-lms/models/src/code_giveaway_codes.rs +++ b/services/headless-lms/models/src/code_giveaway_codes.rs @@ -46,7 +46,7 @@ pub async fn insert_many( query_builder.push_values(input, |mut b, code| { b.push_bind(code_giveaway_id) - .push_bind(code) + .push_bind(code.trim()) .push_bind(added_by_user_id); }); @@ -145,7 +145,7 @@ RETURNING * Ok(res) } -pub async fn stream_code_giveaway_codes<'a>( +pub async fn stream_given_code_giveaway_codes<'a>( conn: &'a mut PgConnection, code_giveaway_id: Uuid, ) -> impl Stream> + 'a { @@ -156,6 +156,7 @@ SELECT * FROM code_giveaway_codes WHERE code_giveaway_id = $1 AND deleted_at IS NULL + AND code_given_to_user_id IS NOT NULL "#, code_giveaway_id ) diff --git a/services/headless-lms/server/src/domain/csv_export/code_giveaway_codes.rs b/services/headless-lms/server/src/domain/csv_export/code_giveaway_codes.rs index 55929559737..d7d180638f3 100644 --- a/services/headless-lms/server/src/domain/csv_export/code_giveaway_codes.rs +++ b/services/headless-lms/server/src/domain/csv_export/code_giveaway_codes.rs @@ -64,7 +64,8 @@ where "code".to_string(), ]); - let mut stream = code_giveaway_codes::stream_code_giveaway_codes(conn, code_giveaway_id).await; + let mut stream = + code_giveaway_codes::stream_given_code_giveaway_codes(conn, code_giveaway_id).await; let writer = CsvWriter::new_with_initialized_headers(writer, headers).await?; while let Some(next) = stream.try_next().await? { diff --git a/services/main-frontend/src/components/page-specific/manage/courses/id/code-giveaway/NewCodeGiveawayForm.tsx b/services/main-frontend/src/components/page-specific/manage/courses/id/code-giveaway/NewCodeGiveawayForm.tsx index 289437d4a5e..6e788a574bd 100644 --- a/services/main-frontend/src/components/page-specific/manage/courses/id/code-giveaway/NewCodeGiveawayForm.tsx +++ b/services/main-frontend/src/components/page-specific/manage/courses/id/code-giveaway/NewCodeGiveawayForm.tsx @@ -22,7 +22,7 @@ const NewCodeGiveawayForm: React.FC = ({ }) => { const [name, setName] = useState("") - const valid = useMemo(() => name.trim() === "", [name]) + const valid = useMemo(() => name.trim() !== "", [name]) const { t } = useTranslation() const createCodeGiveawayMutation = useToastMutation( diff --git a/services/main-frontend/src/pages/manage/code-giveaways/[id]/index.tsx b/services/main-frontend/src/pages/manage/code-giveaways/[id]/index.tsx index 3e2ad713a27..9f71a4d8f0f 100644 --- a/services/main-frontend/src/pages/manage/code-giveaways/[id]/index.tsx +++ b/services/main-frontend/src/pages/manage/code-giveaways/[id]/index.tsx @@ -14,10 +14,6 @@ import ErrorBanner from "@/shared-module/common/components/ErrorBanner" import Spinner from "@/shared-module/common/components/Spinner" import { baseTheme, headingFont, typography } from "@/shared-module/common/styles" -const CodesWrapper = styled.div` - margin: 2rem 0; -` - const CodeGiveawayPage = () => { const router = useRouter() const { id } = router.query @@ -61,16 +57,26 @@ const CodeGiveawayPage = () => { {t("heading-code-giveaway-name", { name: codeGiveawayQuery.data?.name ?? "" })} - + + + + + diff --git a/shared-module/packages/common/src/locales/en/main-frontend.json b/shared-module/packages/common/src/locales/en/main-frontend.json index 4cb0bb225c7..12e506aa5a4 100644 --- a/shared-module/packages/common/src/locales/en/main-frontend.json +++ b/shared-module/packages/common/src/locales/en/main-frontend.json @@ -392,6 +392,7 @@ "link-course-instances": "Course instances", "link-edit-exam-instructions": "Edit exam instructions", "link-exercises": "Exercises", + "link-export-given-codes-as-csv": "Export given codes as CSV", "link-export-completions": "Export completions as CSV", "link-export-course-instances": "Export course instances as CSV", "link-export-course-user-consents": "Export course research consent form answers as CSV",